import Logger from '../base/logger';
import { CCSClient, releaseCcsClient, validateCcsClient, ccsErrorEvent, onTextMessage, onResourceMessage, onHoldOn, onCustomMessage, connect, refreshCcsTicket, sessionMemberJoinQuitEvent, onInvitationProgress, receiveRequestScreenShareMessage, quitSession, resumeCcsConnection, inviteOtherAgent, removeCcsErrorEvent, dealWithCcsSendMessageResponse } from './ccs_client';
import { customerLogin, refreshNewToken, addStreamToMixStream, addBase64ImgToMixStream, removeBase64ImgInMixStream, customerLoginWithOpenId, startAsrEvent, visitorFreeLogin } from './api';
import { addEventListener, dispatchEvent, removeEventListener, systemErrorCode, systemErrorName } from '../base/event';
import {
	createCameraMicStream,
	IRtcClient,
	joinRoom,
	mute,
	publishStream,
	renderRemoteStream,
	unmute,
	zmsAddEventListener,
	zmsRemoveEventListener,
	leaveRoom,
	replaceMediaStream,
	stopMediaStream,
	startLocalRecordWithStreamLabel,
	stopLcoalRecordWithStreamLabel,
	downloadLcoalRecordWithStreamLabel,
	IRTC_CUSTOMEREVENT,
	stopStream,
	renderStreamWithVideoLabel,
	publicationAddEventListener,
	renderStreamAgain,
} from './irtcScript';
import { defaultMediaOptions, mediaOptionsIsValid, ivcsErrorCodeEnum } from '../base/const';
import { deepCopy, isEmptyString, isNullObject, errorlogs, isSupportWebrtc, removeObjectWithId } from './tool';
import { IvcsError } from '../base/error';
import { NetworkMonitoring } from '../base/network';
import { Navigator } from '../base/navigator';
import { MapLocation } from '../modules/map_location';
import { Snapshot } from '../modules/snapshot';
import { onCancelPeerCmd, onPeerCmd, responsePeerCmd } from './peercmd';
const { UAParser } = require('../base/ua-parser');
import { IrtcWhiteBoard } from './whiteboard';
import { ImUpgradeMedia } from './imUpgrade';

/**
 * @version 1.21.0
 * @author <changjian.sun,zhiwen.huang,hui.zhang>
 * @type {object} ivcs访客端相关API
 * {@link https://www.showdoc.com.cn/1499280837202584/7240791073628616|错误码说明}
 * @example 错误码
|  code | message 
| 1000 |参数校验失败  |
| 4000 |网络请求接口失败  |
| 1001 |用户不存在  |
| 1101 |网络请求鉴权失败  |
| 1103 |用户登录密码错误  |
| 1104 |用户账号失效无法登录  |
| 1301 |网络请问参数错误  |
| 1401 |网络请求数据库异常  |
| 5000 |ccsClient未知错误  |
| 5001 |ccsClient未初始化  |
| 5002 |ccsClient-token无效或未初始化  |
| 5003 |ccsClient断开sio链接  |
| 5004 |ccsClient-ticket无效  |
| 5005 |ccsClient-resume失败（重连后刷新链接失败）  |
| 5006 |ccsClient发送消息失败,ccs服务异常,消息无法发送  |
| 5007 |ccsClient初始化链接sio失败  |
| 3000 |amsClient未知错误  |
| 3001 |amsClient未初始化  |
| 3002 |amsClient-token无效或未初始化  |
| 3003 |amsClient断开sio链接  |
| 3004 |amsClient-ticket无效  |
| 3005 |amsClient-resume失败（重连后刷新链接失败）  |
| 3006 |amsClient发送消息失败,ams服务异常,消息无法发送  |
| 3007 |amsClient初始化链接sio失败  |
| 6000 |irtcClient未知错误  |
| 60001 |渲染的流label无效 |
| 6001 |加入房间失败  |
| 6002 |创建本地流失败  |
| 6001 |加入视频房间失败  |
| 6002 |创建视频流失败  |
| 60021 |创建本地流被终止  |
| 60022 |创建本地流不允许，浏览器未授权/系统未授权  |
| 60023 |创建本地流未找到设备  |
| 60024 |创建本地流无法启动设备  |
| 60025 |创建本地流失败，设备分辨率不支持程序设置的分辨率  |
| 60026 |创建本地流失败，安全策略失败，网页链接不安全等  |
| 60027 |创建本地流失败，程序typeError  |
| 6004 |推流失败  |
| 6005 |订阅流失败  |
| 6006 |渲染指定label流的video dom元素不存在  |
| 6007 |视频video自动播放失败  |
| 60070 |mute audio失败  |
| 60071 |mute video失败  |
| 60080 |unmute audio失败  |
| 60081 |unmute video失败  |
| 7000 |SDK内部异常错误  |
 */
class Client {
	/**
	 * 构造方法
	 * @returns new Client
	 */
	constructor() {
		this.networkTool = new NetworkMonitoring();
		this.snapshotModule = new Snapshot();
		this.eventList = new Map();
		this.systemRecordId = null; // 系统录像的recordId
		this.readyToTalkUserList = []; // 告知对方 自己音视频已经准备好
	}

	/**
	 * 判断当前浏览器是否支持webrtc
	 * @returns boolean
	 * @example
	 * console.log('is support', ivcs.client.isSupportWebrtc());
	 */
	isSupportWebrtc() {
		return isSupportWebrtc();
	}

	/**
	 * 测试麦克风是否可用
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败，具体错误原因可参考 {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia}
	 * @example
	 * ivcs.client.microphoneTest((resp)=>{
		 console.log('success',resp);
		},(error)=>{
		 console.log('error',error);
	   });
	 */
	microphoneTest(onSuccess, onFailure) {
		let navigator = new Navigator();
		navigator.audioTest(
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	* 摄像头检测是否可用
	* @param {string} deviceId 摄像头的设备id，默认是传null
	* @param {function} onSuccess 成功
	* @param {function} onFailure 失败，具体错误原因可参考 {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia}
	* @example
	* ivcs.client.cameraTest(null,(resp)=>{
	   console.log('success',resp);
	   },(error)=>{
	   console.log('error',error);
	   });
	*/
	cameraTest(deviceId = null, onSuccess, onFailure) {
		let navigator = new Navigator();
		navigator.videoTest(
			deviceId,
			resp => {
				onSuccess(resp);
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	 * 开启网络测试，并监控网络状态变化
	 * @param {function} networkStatus  网络状态变化回调，返回online=网络已链接，返回offline=网络离线
	 * @returns {boolean} 当前网络是否正常可用
	 * @example 
	 * let online = ivcs.client.startMonitorNetwork((status)=>{
			console.log('now network status',status);
		});
	 */
	startMonitorNetwork(networkStatus) {
		this.networkTool.start(status => {
			networkStatus(status);
		});
		return this.networkTool.online;
	}

	/**
	 * 停止网络测试，停止监控网络状况变化
	 * @example
	 * ivcs.client.stopMonitorNetwork();
	 */
	stopMonitorNetwork() {
		this.networkTool.stop();
	}

	/**
	 * 设置turn server地址， 一般在通话前（startVideo）设置。
	 * @param {string} turnserverUrl  turn server地址
	 * @param {string} turnserverCredential turn server的密码
	 * @param {string} turnserverUsername turn server的用户名
	 */
	setTurnServer(turnserverUrl, turnserverCredential, turnserverUsername) {
		if (this.associatedUrls) {
			this.associatedUrls.turnserverUrl = turnserverUrl;
			this.associatedUrls.turnserverCredential = turnserverCredential;
			this.associatedUrls.turnserverUsername = turnserverUsername;
		} else {
			this.associatedUrls = {
				turnserverUrl: turnserverUrl,
				turnserverCredential: turnserverCredential,
				turnserverUsername: turnserverUsername,
			};
		}
	}

	/**
	 * 访客初始化
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} name 访客注册的用户名
	 * @param {string} password  访客密码
	 * @param {string} org 访客所属的机构
	 * @param {object} mediaOptions 访客端音视频画面的参数，默认传undefined，表示系统默认的分辨率640*480,比特率1024,帧率15
	 * @param {function} onSuccess 初始化成功
	 * @param {function} onFailure 初始化失败
	 * @param {object} islogs  {isOpen:Boolean(是否开启留痕),url:String(留痕的上传服务地址),默认即backendUrl即可}
	 * @example
	 * let isLogs = {
				isOpen:Boolean(是否开启留痕)
				url: String(留痕的上传服务地址)
			};
			ivcs.client.init(
				loginUrl,
				this.name.trim(),
				this.pwd.trim(),
				this.org.trim(),
				defaultMediaOptions,
				resp => {
					console.log('init success', resp);
				},
				error => {
					console.log('init error', error);
				},
				isLogs
			);
	 */
	async init(backendUrl, name, password, org, mediaOptions = defaultMediaOptions, onSuccess, onFailure, islogs) {
		if (backendUrl === undefined || backendUrl === '') {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (name === undefined || name === '') {
			Logger.warning('ivcs-js-sdk:name is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'name is empty');
			onFailure(error);
			return;
		}
		if (password === undefined || password === '') {
			Logger.warning('ivcs-js-sdk:password is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'password is empty');
			onFailure(error);
			return;
		}
		if (org === undefined || org === '') {
			Logger.warning('ivcs-js-sdk:organization is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'organization is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.islogs = deepCopy(islogs);
		try {
			this.backendUrl = deepCopy(backendUrl);
			let login = await customerLogin(backendUrl, name, password, org);
			this.accessToken = login.data && login.data.data && login.data.data.accessToken ? login.data.data.accessToken : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.userId = tokenData.data.id;
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			this.mapLocation = new MapLocation();
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : '';
			this.ccsUrl = backendUrl ? backendUrl : login.data.data.urls.ccsurl;
			let refreshTicketTimeMs = tokenData.exp - tokenData.iat - 1 > 0 ? (tokenData.exp - tokenData.iat - 1) * 1000 : 1000;
			let intervalMax = Math.pow(2, 31) - 1;
			refreshTicketTimeMs > intervalMax ? (refreshTicketTimeMs = intervalMax) : undefined;
			this.refreshTokenInterval = setInterval(async () => {
				try {
					let newTokenTicket = await refreshNewToken(backendUrl, this.userId, this.ticket);
					this.ticket = newTokenTicket.ticket;
					this.accessToken = newTokenTicket.accessToken;
					this.ccsClient.accessToken = newTokenTicket.accessToken;
				} catch (error) {
					clearInterval(this.refreshTokenInterval);
					onFailure(error);
				}
			}, refreshTicketTimeMs);
			onSuccess(login.data.data);
		} catch (error) {
			Logger.error(`ivcs-js-sdk:init catch error:${error},time:${new Date().toISOString()}`);
			onFailure(error);
		}
	}

	/**
	 * 访客(游客)免密初始化
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} userId  访客id
	 * @param {string} org 访客所属的机构
	 * @param {object} mediaOptions 访客端音视频画面的参数，默认传undefined，表示系统默认的分辨率640*480,比特率1024,帧率15
	 * @param {function} onSuccess 初始化成功
	 * @param {function} onFailure 初始化失败 {1301:'校验失败'，‘404’:‘机构不存在’}
	 * @param {object} islogs  {isOpen:Boolean(是否开启留痕),url:String(留痕的上传服务地址),默认即backendUrl即可}
	 * @example
	 * let isLogs = {
				isOpen:Boolean(是否开启留痕)
				url: String(留痕的上传服务地址)
			};
			ivcs.client.initWithoutPassword(
				loginUrl,
				this.userId，
				this.org,
				defaultMediaOptions,
				resp => {
					console.log('initWithoutPassword success', resp);
				},
				error => {
					console.log('initWithoutPassword error', error);
				},
				isLogs
			);
	 */
	async initWithoutPassword(backendUrl, userId, org, mediaOptions = defaultMediaOptions, onSuccess, onFailure, islogs) {
		if (backendUrl === undefined || backendUrl === '') {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (userId === undefined || userId === '') {
			Logger.warning('ivcs-js-sdk:userId is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'userId is empty');
			onFailure(error);
			return;
		}
		if (org === undefined || org === '') {
			Logger.warning('ivcs-js-sdk:organization is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'organization is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.islogs = deepCopy(islogs);
		try {
			this.backendUrl = deepCopy(backendUrl);
			let login = await visitorFreeLogin(backendUrl, userId, org);
			this.accessToken = login.data && login.data.data && login.data.data.accessToken ? login.data.data.accessToken : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.userId = tokenData.data.id;
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			this.mapLocation = new MapLocation();
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : '';
			this.ccsUrl = backendUrl ? backendUrl : login.data.data.urls.ccsurl;
			let refreshTicketTimeMs = tokenData.exp - tokenData.iat - 1 > 0 ? (tokenData.exp - tokenData.iat - 1) * 1000 : 1000;
			let intervalMax = Math.pow(2, 31) - 1;
			refreshTicketTimeMs > intervalMax ? (refreshTicketTimeMs = intervalMax) : undefined;
			this.refreshTokenInterval = setInterval(async () => {
				try {
					let newTokenTicket = await refreshNewToken(backendUrl, this.userId, this.ticket);
					this.ticket = newTokenTicket.ticket;
					this.accessToken = newTokenTicket.accessToken;
					this.ccsClient.accessToken = newTokenTicket.accessToken;
				} catch (error) {
					clearInterval(this.refreshTokenInterval);
					onFailure(error);
				}
			}, refreshTicketTimeMs);
			onSuccess(login.data.data);
		} catch (error) {
			Logger.error(`ivcs-js-sdk:initWithoutPassword catch error:${JSON.stringify(error)},time:${new Date().toISOString()}`);
			onFailure(error);
		}
	}

	/**
	 * 游客初始化,适用于非注册用户,外系统的访客调用ivcs登录接口后返回相关的参数。
	 * @param {string} backendUrl  请求的后台的地址
	 * @param {object} associatedUrls  后台服务相关联地址
	 * @param {string} accessToken  游客登录后返回的accessToken
	 * @param {object} tokenData 游客登录后返回的tokenData
	 * @param {string} ticket  游客的token对应的刷新的ticket
	 * @param {object} mediaOptions 访客端音视频画面的参数，默认传undefined，表示系统默认的分辨率640*480,比特率1024,帧率15
	 * @param {function} onSuccess  游客初始化成功
	 * @param {function} onFailure  游客初始化失败
	 * @param {object} islogs 是否留痕相关日志,留痕的相关配置 {isOpen:Boolean(是否开启留痕),url:String(留痕的上传服务地址),默认即backendUrl即可}
	 * @example
	 *  let backendUrl = document.getElementById('backend_url_id').value;
		let accessToken =
			'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo1MCwidXNlck5hbWUiOiIwMDEiLCJvcmciOiIxIiwicm9sZSI6ImN1c3RvbWVyIiwiaXNEaXNhYmxlZCI6Ik4iLCJpc1JvYm90IjpmYWxzZSwicHJvZmlsZSI6eyJnZW5kZXIiOiLlpbMiLCJwaWN0dXJlIjoiaHR0cDovL3plYWxjb21tLWZpbGV1cGxvYWQub3NzLWNuLXNoYW5naGFpLmFsaXl1bmNzLmNvbS8yZjY1ZmY3YWFlMjYwYjk1YjY5NzYyMDBkNThiNzNiNiIsImZ1bGxOYW1lIjoi5a2Z6LCi6LCiIn0sImN1c3RvbWVyTGV2ZWwiOjEsIl9pZCI6NTAsInNjaGVkdWxpbmdBdHRyaWJ1dGVzIjp7fX0sImlhdCI6MTYyNzcxMzgxOCwiZXhwIjoxNjI3NzU3MDE4fQ.f_rb5hBPz_kHrH8iQUSkXy5zPsQn15cA9qpAt2odrAg';
		let tokenData = {
			data: {
				id: 50,
				userName: '001',
				org: '1',
				role: 'customer',
				isDisabled: 'N',
				isRobot: false,
				profile: { isRobot: false },
				customerLevel: null,
				_id: 50,
				schedulingAttributes: {
					是否机器人: 'false',
				},
			},
			exp: 1627757018,
			iat: 1627713818,
		};
		let associatedUrls = {
			backendurl: 'https://ivcs-xxx.xxx.xx',
			ccsurl: 'https://ivcs-xxx.xxx.xx',
			domainType: 'public',
			icbcAxaBackendUrl: 'https://localhost:3095',
			turnserverCredential: 'test',
			turnserverUrl: 'turn:localhost?transport=tcp',
			turnserverUsername: 'test',
			uploadUrl: 'https://ivcs-xxx.xxx.xx',
		};
		let ticket =
			'RWqmtFeTqB7LDq7IuY6McZK44KoWLVzSeYUyJRcF9eQz3F_9b-zSWroDJr2Og2fs';
		let mediaOptions = undefined;
		let isLogs = {
				isOpen:Boolean(是否开启留痕)
				url: String(留痕的上传服务地址)
			};
		ivcs.client.visitorInit(
			backendUrl,
			associatedUrls,
			accessToken,
			tokenData,
			ticket,
			mediaOptions,
			(resp) => {
				console.log('visitorInit success', resp);
			},
			(error) => {
				console.log('visitorInit failed', error);
			},
			isLogs
		);
	 */
	async visitorInit(backendUrl, associatedUrls, accessToken, tokenData, ticket, mediaOptions = defaultMediaOptions, onSuccess, onFailure, islogs) {
		if (isEmptyString(backendUrl)) {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (isEmptyString(accessToken)) {
			Logger.warning('ivcs-js-sdk:accessToken is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'accessToken is empty');
			onFailure(error);
			return;
		}
		if (isEmptyString(ticket)) {
			Logger.warning('ivcs-js-sdk:ticket is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'ticket is empty');
			onFailure(error);
			return;
		}
		if (isNullObject(associatedUrls)) {
			Logger.warning('ivcs-js-sdk:associatedUrls is error');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'associatedUrls is error');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.accessToken = deepCopy(accessToken);
		this.islogs = deepCopy(islogs);
		if (!tokenData) {
			Logger.warning('ivcs-js-sdk:accessToken is Invalid Signature');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'accessToken is Invalid Signature');
			onFailure(error);
			return;
		}
		try {
			this.backendUrl = deepCopy(backendUrl);
			this.userId = tokenData.data.id;
			this.ticket = deepCopy(ticket);
			this.mapLocation = new MapLocation();
			this.associatedUrls = deepCopy(associatedUrls);
			this.ccsUrl = backendUrl ? backendUrl : this.associatedUrls.ccsurl;
			let refreshTicketTimeMs = tokenData.exp - tokenData.iat - 1 > 0 ? (tokenData.exp - tokenData.iat - 1) * 1000 : 1000;
			let intervalMax = Math.pow(2, 31) - 1;
			refreshTicketTimeMs > intervalMax ? (refreshTicketTimeMs = intervalMax) : undefined;
			this.refreshTokenInterval = setInterval(async () => {
				try {
					let newTokenTicket = await refreshNewToken(backendUrl, this.userId, this.ticket);
					this.ticket = newTokenTicket.ticket;
					this.accessToken = newTokenTicket.accessToken;
					this.ccsClient.accessToken = newTokenTicket.accessToken;
				} catch (error) {
					clearInterval(this.refreshTokenInterval);
					onFailure(error);
				}
			}, refreshTicketTimeMs);
			onSuccess('ok');
		} catch (error) {
			Logger.error(`ivcs-js-sdk:visitorInit catch error:${error},time:${new Date().toISOString()}`);
			onFailure(error);
		}
	}

	/**
	 * 非注册用户初始化
	 * @param {string} backendUrl 请求的后台服务地址
	 * @param {string} openId 非注册用户的openId，如小程序用户openId，用户mobilePhone等，默认为undefined
	 * @param {object} mediaOptions 访客端音视频画面的参数，默认传undefined，表示系统默认的分辨率640*480,比特率1024,帧率15
	 * @param {function} onSuccess 初始化成功 {ticket:string(ticket),ticketExpiresIn:number(ticketExpiresIn),token:string(token),tokenData:object(tokenData),urls:object(urls)}
	 * @param {function} onFailure 初始化失败
	 * @param {object} islogs  {isOpen:Boolean(是否开启留痕),url:String(留痕的上传服务地址),默认即backendUrl即可}
	 * @param {string} org  所属机构
	 * @example
	 *  ivcs.client.unRegisteredInit(
			backendUrl,
			'openId123',
			undefined,
			(resp) => {
				console.log('unRegisteredInit success', resp);
			},
			(error) => {
				console.log('unRegisteredInit failed', error);
			},
			{
				isOpen: false,
			}
		);
	 */
	async unRegisteredInit(backendUrl, openId = undefined, mediaOptions = defaultMediaOptions, onSuccess, onFailure, islogs, org = '') {
		if (isEmptyString(backendUrl)) {
			Logger.warning('ivcs-js-sdk:backendUrl is empty');
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'backendUrl is empty');
			onFailure(error);
			return;
		}
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		this.islogs = deepCopy(islogs);
		try {
			this.backendUrl = deepCopy(backendUrl);
			let login = await customerLoginWithOpenId(backendUrl, openId, org);
			this.accessToken = login.data && login.data.data && login.data.data.token ? login.data.data.token : '';
			let tokenData = login.data && login.data.data && login.data.data.tokenData ? login.data.data.tokenData : null;
			this.userId = tokenData.data.id;
			this.ticket = login.data && login.data.data && login.data.data.ticket ? login.data.data.ticket : '';
			this.mapLocation = new MapLocation();
			this.associatedUrls = login.data && login.data.data && login.data.data.urls ? login.data.data.urls : '';
			this.ccsUrl = backendUrl ? backendUrl : login.data.data.urls.ccsurl;
			let refreshTicketTimeMs = tokenData.exp - tokenData.iat - 1 > 0 ? (tokenData.exp - tokenData.iat - 1) * 1000 : 1000;
			let intervalMax = Math.pow(2, 31) - 1;
			refreshTicketTimeMs > intervalMax ? (refreshTicketTimeMs = intervalMax) : undefined;
			this.refreshTokenInterval = setInterval(async () => {
				try {
					let newTokenTicket = await refreshNewToken(backendUrl, this.userId, this.ticket);
					this.ticket = newTokenTicket.ticket;
					this.accessToken = newTokenTicket.accessToken;
					this.ccsClient.accessToken = newTokenTicket.accessToken;
				} catch (error) {
					clearInterval(this.refreshTokenInterval);
					onFailure(error);
				}
			}, refreshTicketTimeMs);
			onSuccess(login.data.data);
		} catch (error) {
			Logger.error(`ivcs-js-sdk:unRegisteredInit catch error:${error},time:${new Date().toISOString()}`);
			onFailure(error);
		}
	}

	/**
	 * 初始化建立呼叫连接
	 * @param {function} onSuccess 成功 'ok'
	 * @param {function} onFailure 失败
	 */
	async initConnect(onSuccess, onFailure) {
		this.ccsClient = new CCSClient();
		this.imUpgradeMedia = new ImUpgradeMedia();
		this.requestProgressLogs = [];
		Logger.info(`ivcs-js-sdk:start-connect-ccs:${new Date().toISOString()}`);
		this.requestProgressLogs.push(`start-connect-ccs:${new Date().toISOString()}`);
		const reconnectErrorTime = 10;
		// eslint-disable-next-line no-unused-vars
		let connectCcs = await connect(
			this.ccsClient,
			this.ccsUrl,
			this.accessToken,
			error => {
				onFailure(error);
				return;
			},
			reconnectErrorTime
		);
		Logger.info(`ivcs-js-sdk:connect-ccs-success:${new Date().toISOString()}`);
		this.requestProgressLogs.push(`connect-ccs-success:${new Date().toISOString()}`);
		onSuccess('ok');
	}

	/**
	 * 发起呼叫
	 * @param {object} requestParameter  发起呼叫的参数，可自定义随路数据(userData)，必填呼叫的业务场景id(service)，根据呼叫码呼叫可不填。
	 * @param {function} onSuccess 成功 {clientId,members,sessionId,refersh_ticket_time,ticket}
	 * @param {function} onFailure 失败 {code,message}; code错误解释
	 * <'5006':'消息发送失败','501':'根据呼叫码方式呼叫，呼叫码已经过期','502':'根据呼叫码方式呼叫，业务场景停用','503':'根据呼叫码方式呼叫，业务场景无效',
	 * '504':'根据呼叫码方式呼叫，呼叫的业务场景下无匹配的坐席','505':'根据呼叫码方式呼叫，呼叫码已经被使用','506':'根据呼叫码方式呼叫,还未到开始时间',
	 * '507':'根据呼叫码方式呼叫,呼叫码错误','601':'业务流水号呼叫，双录已完成并通过质检','602':'业务流水号呼叫，双录已完成并等待质检',
	 * '603':'业务流水号呼叫，双录进行中，请勿重复操作！','604':'业务流水号呼叫，补录进行中，请勿重复操作！','420':'指定坐席并且指定了标签呼叫，派工时发现指定的坐席没有绑定该标签', '421':'指定了分组链呼叫时，对应业务场景下不包含分组链中的任何分组', '422':'呼叫指定坐席，但是坐席未绑定呼叫的分组！', '423':'呼叫指定坐席，坐席未签入！'
	 * '401':呼叫超时; '404':未找到坐席;'406':呼叫非工作日;'407':呼叫非工作时间;'408':呼叫业务场景不可用;'409':呼叫超过上限;'410':呼叫坐席拒绝;'411':呼叫其他访客接听;'412':多访客下有机器人存在;'413':呼叫坐席拒绝;'414':呼叫振铃超时;'415':呼叫取消;
	 * '416':访客角色为空（呼叫）;'417':场景角色为空（呼叫、转接）;'418':场景角色不匹配（呼叫、转接）;'419':会话中当前角色已存在;'101':'黑名单呼叫', 106:'链接ccs失败'
	 * >
	 * @param {number} reconnectErrorTime websocket允许错误重连次数,默认10次
	 * @example
	 * let userData = {
				media: 'video',//'im','audio','video'
				//携带的业务数据，如fullName，可替换机器人话术中配置的变量，如fullName:张三，则后管机器人话术中配置的，你是{{fullName}}吗,会被播报成你是张三吗。
				//userData数据会透传给接听的坐席，可用于做一些业务处理，如根据用户userId。查询用户的相关信息等。
				userData: {
					virtualAgentType: 'unregistered',
					businessflowId: '',// 业务流水号
					callCode: 'xxx', // 根据呼叫码呼叫
					service: ,//业务场景id
					specifyAgent: ,//指定的坐席id
					userId: ,//用户系统的用户id
					callMediaType: ,//访客呼叫的媒体类型，audio/video
					receiveMediaType: ,//坐席接听的媒体类型 audio/video
					fullName: ,//用户系统的用户昵称
					gender: ,//用户系统的用户性别，
					isScreenSyncMode: ,//是否同屏呼叫
					clientInfo: ,//呼叫的终端信息，如h5/小程序/安卓/iOS
					filter: [
							{
								attribute: '是否机器人',
								rule: 'ne',
								value: 'true'
							}
					],//过滤呼叫非机器人坐席 
				},
			};
			ivcs.client.requestSession(
				userData,
				resp => {
					console.log(`'request session success',${JSON.stringify(resp)},${new Date()}`);
				},
				error => {
					console.log(`'request session error',${JSON.stringify(error)},${new Date()}`);
				}
			);
	 */
	async requestSession(requestParameter, onSuccess, onFailure, reconnectErrorTime = 10) {
		if (isEmptyString(this.ccsUrl)) {
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_CCS_UNDEFINED, 'please init');
			onFailure(error);
			return;
		}
		if (isNullObject(requestParameter)) {
			let error = new IvcsError(ivcsErrorCodeEnum.ERROR_PARAMETER, 'requestParameter is invalid');
			onFailure(error);
			return;
		}
		try {
			let ccsEventList = ccsErrorEvent(info => {
				Logger.error('ivcs-js-sdk:ccs error event', info);
				dispatchEvent(systemErrorName, systemErrorCode.ccsSocketIoError, info);
			});
			let isValid = validateCcsClient(this.ccsClient);
			if (isValid != 'ok') {
				onFailure(isValid);
				return;
			}
			if (requestParameter.userData) {
				const uaParser = new UAParser();
				const ua = uaParser.getResult();
				requestParameter.userData.ua = ua;
				if (requestParameter.userData.customerRole) {
					this.customerRole = requestParameter.userData.customerRole;
				} else {
					this.customerRole = '';
				}
				if (requestParameter.userData.userName) {
					this.customerName = requestParameter.userData.userName;
				} else {
					this.customerName = '';
				}
			}
			Logger.info(`ivcs-js-sdk:request-progress-start-request-session:${new Date().toISOString()}`);
			this.requestProgressLogs.push(`request-progress-start-request-session:${new Date().toISOString()}`);
			this.ccsClient.requestSession(requestParameter, (status, resp) => {
				if (status === 'ok') {
					if (resp && resp.hasAvailableAgent) {
						this.roomId = resp.roomId;
						this.roomToken = resp.roomToken;
						this.sessionId = resp.sessionId;
						window.ivcsSessionId = this.sessionId;
						this.ccsClient.ticket = resp.ticket;
						let refreshTicketTime = resp.refersh_ticket_time;
						refreshCcsTicket(this.ccsClient, refreshTicketTime);
						this.ccsClient.members = resp.members;
						sessionMemberJoinQuitEvent(
							this.ccsClient,
							quitCustomer => {
								Logger.info(`ivcs-js-sdk:customer:${JSON.stringify(quitCustomer)} quit session,at:${new Date().toISOString()}`);
								removeObjectWithId(this.ccsClient.members, quitCustomer.id);
							},
							agent => {
								this.systemRecordId = agent.recordingId ? agent.recordingId : this.systemRecordId;
								this.roomId = agent.roomId ? agent.roomId : this.roomId;
								this.roomToken = agent.roomToken ? agent.roomToken : this.roomToken;
								Logger.info(`ivcs-js-sdk:agent:${JSON.stringify(agent)} join session with roomId:${this.roomId},roomToken:${this.roomToken},at:${new Date().toISOString()}`);
								this.ccsClient.members = [...this.ccsClient.members, agent];
								// this.ccsClient.sendZmsRoomEvent({ event: 'publish-success' }, (s, r, m) => {
								// 	Logger.info(`ivcs-js-sdk:send zms-room-event publish-success event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
								// });
							},
							joinCustomer => {
								this.ccsClient.members = [...this.ccsClient.members, joinCustomer];
							}
						);
						Logger.info(`ivcs-js-sdk:request-progress-request-session-success:${new Date().toISOString()},result:${status},${resp}`);
						this.requestProgressLogs.push(`request-progress-request-session-success:${new Date().toISOString()}`);
						this.ccsClient.onSessionIdChange(data => {
							Logger.info(`ivcs-js-sdk:onSessionIdChange data:${data},time:${new Date().toISOString()}`);
							if (data?.data?.sessionId && this.irtcClient) {
								this.irtcClient.setAllSubscribeCallQuality(deepCopy(this.irtcClient.sessionId));
								this.irtcClient.sessionId = data.data.sessionId;
								this.systemRecordId = null;
							}
						});
						onSuccess(resp);
					} else {
						// 判断是否是黑名单
						if (resp.ban) {
							const result = new IvcsError('101', resp.msg);
							onFailure(result);
						} else {
							const status = resp.invitationStatus;
							const result = new IvcsError('400', status);
							switch (status) {
								case 'timeout':
									result.code = '401';
									break;
								case 'notfound':
									result.code = '404';
									break;
								case 'offday':
									result.code = '406';
									break;
								case 'offtime':
									result.code = '407';
									break;
								case 'scenariodisabled':
									result.code = '408';
									break;
								case 'outofquota':
									result.code = '409';
									break;
								case 'outofquota':
									result.code = '401';
									break;
								case 'agentrejected':
									result.code = '410';
									break;
								case 'otherCustomerAccept':
									result.code = '411';
									break;
								case 'multiCustomerForbidAI':
									result.code = '412';
									break;
								case 'rejected':
									result.code = '413';
									break;
								case 'ringingTimeout':
									result.code = '414';
									break;
								case 'canceled':
									result.code = '415';
									break;
								case 'customerRolesEmpty':
									result.code = '416';
									break;
								case 'scenarioRolesEmpty':
									result.code = '417';
									break;
								case 'scenarioRolesNotMatched':
									result.code = '418';
									break;
								case 'scenarioRoleHasExistedInSession':
									result.code = '419';
									break;
								case 'labelsDoNotMatch':
									result.code = '420';
									break;
								case 'groupsDoNotMatch':
									result.code = '421';
									break;
								case 'agentNotBoundGroupsInScenario':
									result.code = '422';
									break;
								case 'agentNotCheckInGroupsInScenario':
									result.code = '423';
									break;
								default:
									break;
							}
							onFailure(result);
						}
					}
				} else {
					// http://192.168.31.91:8090/pages/viewpage.action?pageId=6063298
					Logger.info(`ivcs-js-sdk:request-progress-request-session-error:${new Date().toISOString()},result:${status},${resp}`);
					this.islogs && this.islogs.isOpen && errorlogs('request-session-error' + resp, this.islogs, this.sessionId);
					if (status === 'codeExpired') {
						onFailure(new IvcsError('501', resp));
					} else if (status === 'scenariodisabled') {
						onFailure(new IvcsError('502', resp));
					} else if (status === 'bizIdInvalid') {
						onFailure(new IvcsError('503', resp));
					} else if (status === 'agentIdInvalidInBiz') {
						onFailure(new IvcsError('504', resp));
					} else if (status === 'callCodeUsed') {
						onFailure(new IvcsError('505', resp));
					} else if (status === 'codeNotStarted') {
						onFailure(new IvcsError('506', resp));
					} else if (status === 'callCodeNotExists') {
						onFailure(new IvcsError('507', resp));
					} else if (status === 'audioVideoRecordingCompleted') {
						onFailure(new IvcsError('601', resp));
					} else if (status === 'audioVideoRecordingWaited') {
						onFailure(new IvcsError('602', resp));
					} else if (status === 'audioVideoRecordingInProgress') {
						onFailure(new IvcsError('603', resp));
					} else if (status === 'audioVideoRerecordingInProgress') {
						onFailure(new IvcsError('604', resp));
					} else {
						dealWithCcsSendMessageResponse(status, resp, onFailure);
					}
					removeCcsErrorEvent(ccsEventList);
				}
			});
		} catch (error) {
			Logger.error('ivcs-js-sdk:request session error', error, 'at:', new Date().toISOString());
			onFailure(new IvcsError(ivcsErrorCodeEnum.ERROR_SDK_CATCH, error.name || error.message || error));
			this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId ? this.sessionId : new Date().toISOString());
		}
	}

	/**
	 * 媒体切换,常用于im通话升级成语音/视频通话
	 * @param {object} data {media:'im','audio','video'}
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	requestMediaSwitch(data, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.requestMediaSwitch(data, (status, resp) => {
			if (status == 'ok') {
				this.roomToken = resp.roomToken;
				this.roomId = resp.roomId;
				this.systemRecordId = resp.recordingId;
				onSuccess(resp);
			} else {
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听媒体切换事件
	 * @param {function} ack
	 * {
		  "type": "media-switch",
		  "from": "system",
		  "to": ....,
		  "data": {
			 status: "upgrade",
			 type: "im" | "video" | "audio",
			 room: ...,
			 roomToken: ...,
			 at: ...
		  }
		}
	 */
	onMediaSwitch(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			return;
		}
		this.ccsClient.onMediaSwitch(resp => {
			this.roomId = resp.data && resp.data.roomId ? resp.data.roomId : '';
			this.roomToken = resp.data && resp.data.roomToken ? resp.data.roomToken : '';
			this.systemRecordId = resp.data && resp.data.recordingId ? resp.data.recordingId : '';
			ack(resp);
		});
	}

	/**
	 * 发起呼叫后，相关的进度回调，状态变化(一般在requestSession后就监听此回调方法)
	 * @param {function} ack 呼叫的进度回调,状态数据
	 * @example
	 * {'401:'转接超时',402':'等待其他坐席接听','403':"坐席正在振铃中",'404':'未找到坐席','406':'转接的业务场景非工作日','407':'转接的业务场景非工作时间','408':'转接的业务场景不可用','409':'呼叫已到上限','410':'坐席拒绝接听,用于指定坐席呼叫','411':'其他访客已经接通,用于呼叫码呼叫，多个访客呼叫，其中一个接听后。其他访客收到该状态回调', '416':'指定了分组链呼叫时，对应业务场景下不包含分组链中的任何分组', '417':' 指定坐席并且指定了标签呼叫，派工时发现指定的坐席没有绑定该标签'}
	 ivcs.client.onInvitationProgress(resp => {
				console.log('invite progress', resp);
				let code = resp.code;
				//code 402 = waiting
				if (code === '401') {
				} else if (code === '403') {
					console.log('agent is connecting');
				} else if (code === '404') {
				} else if (code === '402') {
					var waitingCode = resp.waitingCode;
					this.waitingNumber = resp.waitingNumber;
					this.currentPosition = resp.position;
					if (waitingCode) {
						if (waitingCode === '1000') {
							//正在分配坐席
						} else if (waitingCode === '1001') {
							//进入排队系统
						} else if (waitingCode === '1002') {
							//指定坐席，坐席没登录
						} else if (waitingCode === '1003') {
							//指定坐席，坐席当前状态不可用，agentStatus=ringing/serving/notReady/otherWork
						} else if (waitingCode === '1004') {
							//指定坐席，坐席振铃时,未相应
						}
					}
				} else if (code === '405') {
				} else if (code === '406') {
				} else if (code === '407') {
				} else if (code === '408') {
				}
			});
	 */
	onInvitationProgress(ack) {
		onInvitationProgress(this.ccsClient, resp => {
			Logger.info(`ivcs-js-sdk:request-progress-on-invitationProgress,info:${JSON.stringify(resp)},time:${new Date().toISOString()}`);
			this.requestProgressLogs.push(`request-progress-on-invitationProgress:${new Date().toISOString()},${JSON.stringify(resp)}`);
			ack(resp);
		});
	}

	/**
	 * 加入一个已经存在的会话
	 * @param {object} joinParameter 加入会话的参数 {userData:{inviteCode:'',customerRole:''}}
	 * @param {function} onSuccess 加入会话成功
	 * @param {function} onFailure  加入会话失败 {5006:消息发送失败,5008:不存在的会话,501:邀请码不存在；502:邀请码已经被使用；503:邀请码还未到开始时间；504:邀请码已过期；505:无效的会话；506:会话已结束；507:通话中存在AI坐席;508:会话中当前角色已存在}
	 * @param {number} reconnectErrorTime websocket允许错误重连次数 默认10次
	 * @example
	 * ivcs.client.joinSession(
			{
				userData:{inviteCode:'xxx',customerRole:'普通访客'}
			},
			resp => {
				console.log('joinSession success', resp);
			},
			error => {
				console.log('joinSession error,', error);
			}
		);
	 */
	async joinSession(joinParameter, onSuccess, onFailure, reconnectErrorTime = 10) {
		try {
			this.ccsClient = new CCSClient();
			this.imUpgradeMedia = new ImUpgradeMedia();
			this.requestProgressLogs = [];
			// eslint-disable-next-line no-unused-vars
			let connectCcs = await connect(
				this.ccsClient,
				this.ccsUrl,
				this.accessToken,
				error => {
					onFailure(error);
					return;
				},
				reconnectErrorTime
			);
			let ccsEventList = ccsErrorEvent(info => {
				Logger.error('ivcs-js-sdk:ccs error event', info);
				dispatchEvent(systemErrorName, systemErrorCode.ccsSocketIoError, info);
			});
			let isValid = validateCcsClient(this.ccsClient);
			if (isValid != 'ok') {
				onFailure(isValid);
				return;
			}
			if (joinParameter.userData) {
				const uaParser = new UAParser();
				const ua = uaParser.getResult();
				joinParameter.userData.ua = ua;
				if (joinParameter.userData.customerRole) {
					this.customerRole = joinParameter.userData.customerRole;
				} else {
					this.customerRole = '';
				}
				if (joinParameter.userData.userName) {
					this.customerName = joinParameter.userData.userName;
				} else {
					this.customerName = '';
				}
			}
			this.ccsClient.joinSession(joinParameter, (status, resp) => {
				Logger.info('ivcs-js-sdk:join session result,', status, resp);
				this.requestProgressLogs = []; //初始化日志
				if (status === 'ok') {
					this.ccsClient.ticket = resp.ticket;
					let refreshTicketTime = resp.refersh_ticket_time;
					this.roomId = resp.roomId;
					this.roomToken = resp.roomToken;
					this.customerRole = resp.customerRole;
					// this.customerName = resp.customerName; // 邀请码产生的访客名
					this.inviteBizScenId = resp.bizScenId; // 邀请码产生的业务场景id
					this.inviteOrg = resp.org; // 邀请码产生的机构名
					this.inviteSession = resp.sessionId; // 邀请码产生的sessionId
					this.sessionId = resp.sessionId;
					this.ccsClient.members = resp.members;
					sessionMemberJoinQuitEvent(
						this.ccsClient,
						quitCustomer => {
							Logger.info(`ivcs-js-sdk:customer:${JSON.stringify(quitCustomer)} quit session,at:${new Date().toISOString()}`);
							removeObjectWithId(this.ccsClient.members, quitCustomer.id);
						},
						agent => {
							this.systemRecordId = agent.recordingId ? agent.recordingId : this.systemRecordId;
							this.roomId = agent.roomId ? agent.roomId : this.roomId;
							this.roomToken = agent.roomToken ? agent.roomToken : this.roomToken;
							Logger.info(`ivcs-js-sdk:agent:${JSON.stringify(agent)} join session with roomId:${this.roomId},roomToken:${this.roomToken},at:${new Date().toISOString()}`);
							this.ccsClient.members = [...this.ccsClient.members, agent];
							this.ccsClient.sendZmsRoomEvent({ event: 'publish-success' }, (s, r, m) => {
								Logger.info(`ivcs-js-sdk:send zms-room-event publish-success event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
							});
						},
						joinCustomer => {
							this.ccsClient.members = [...this.ccsClient.members, joinCustomer];
						}
					);
					refreshCcsTicket(this.ccsClient, refreshTicketTime);
					this.ccsClient.onSessionIdChange(data => {
						Logger.info(`ivcs-js-sdk:onSessionIdChange data:${data},time:${new Date().toISOString()}`);
						if (data?.data?.sessionId && this.irtcClient) {
							this.irtcClient.setAllSubscribeCallQuality(deepCopy(this.irtcClient.sessionId));
							this.irtcClient.sessionId = data.data.sessionId;
							this.systemRecordId = null;
						}
					});
					onSuccess(resp);
				} else {
					if (status === 'inviteCodeNotExists') {
						onFailure(new IvcsError('501', resp));
					} else if (status === 'inviteCodeUsed') {
						onFailure(new IvcsError('502', resp));
					} else if (status === 'inviteCodeNotStarted') {
						onFailure(new IvcsError('503', resp));
					} else if (status === 'inviteCodeExpired') {
						onFailure(new IvcsError('504', resp));
					} else if (status === 'invalidSession') {
						onFailure(new IvcsError('505', resp));
					} else if (status === 'sessionClosed') {
						onFailure(new IvcsError('506', resp));
					} else if (status === 'virtualAgentExists') {
						onFailure(new IvcsError('507', resp));
					} else if (status === 'scenarioRoleHasExistedInSession') {
						// 会话中场景角色已存在（join session）
						onFailure(new IvcsError('508', resp));
					} else {
						if (resp === 'Non-exist session') {
							onFailure(new IvcsError(ivcsErrorCodeEnum.ERROR_CCS_NOT_EXIST_SESSION, resp));
						} else {
							dealWithCcsSendMessageResponse(status, resp, onFailure);
						}
					}
					removeCcsErrorEvent(ccsEventList);
				}
			});
		} catch (error) {
			Logger.error('ivcs-js-sdk:join session error', error, 'at:', new Date());
			onFailure(error);
		}
	}

	/**
	 * 离开会话，一般是访客主动挂断会话/或者坐席都已离开后调用
	 * @param {object} reason 挂断原因 如:{reason:"业务已完成"},默认传值为{}
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 * @example
	 * ivcs.client.quitSession(
					{},
					resp => {
						console.log('quit session', resp);
					},
					error => {
						console.log('error,', error);
					}
		);
	 */
	quitSession(reason = {}, onSuccess, onFailure) {
		this.systemRecordId = null;
		this.irtcWhiteBoard = null;
		this.readyToTalkUserList = [];
		this.islogs && this.islogs.isOpen && errorlogs(this.requestProgressLogs, this.islogs, this.sessionId);
		this.irtcClient ? zmsRemoveEventListener(this.irtcClient) : undefined;
		this.stopMonitorNetwork();
		let taskList = [];
		if (this.irtcClient) {
			taskList.push(leaveRoom(this.irtcClient));
			//release subscription
			this.irtcClient.subscriptionList &&
				this.irtcClient.subscriptionList.forEach((value, key) => {
					let streamlabel = key;
					let intervalId = value.getStatsIntervalId;
					this.stopRemoteStreamStats(
						streamlabel,
						intervalId,
						() => {},
						() => {}
					);
				});
			this.irtcClient.subscriptionList = null;
		}
		if (this.ccsClient && this.ccsClient.accessToken) {
			taskList.push(quitSession(reason, this.ccsClient));
		}
		if (this.localStream) {
			stopMediaStream(this.localStream);
		}
		//remove event-listener
		this.eventList &&
			this.eventList.forEach((value, key) => {
				this.removeEventListener(key, value);
			});
		Promise.allSettled(taskList)
			.then(value => {
				Logger.info('ivcs-js-sdk:quit session success', value);
				onSuccess(value);
			})
			.catch(error => {
				Logger.info('ivcs-js-sdk:quit session error', error);
				this.islogs && this.islogs.isOpen && errorlogs('quit session all task catch error ', this.islogs, this.sessionId);
				this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
				onFailure(error);
			});
		if (this.streamAddListener) {
			removeEventListener(IRTC_CUSTOMEREVENT.STREAMADD, this.streamAddListener);
		}
		if (this.streamAsrListener) {
			removeEventListener(IRTC_CUSTOMEREVENT.ASR, this.streamAsrListener);
		}
		this.clearSessionData();
	}

	/**
	 * 告知对方本端已经推流成功，并成功订阅对方画面。
	 * @param {object} user 访客自定义的一些个人信息（随路数据），如：头像，姓名，性别等,默认传{} 空对象即可
	 * @param {string} toWho 告知消息的对象，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人
	 * @param {function} onSuccess 消息发送成功
	 * @param {function} onFailure 消息发送失败 code:5014 = 未调用startVideo
	 * @example
	 * let user = {
						picture: localStorage.getItem('userPicture'),
						fullName: localStorage.getItem('fullName'),
						gender: localStorage.getItem('gender')
				   };
	   ivcs.client.readyToTalk(
					user,//用户信息。非必填，可传{}
					agentClientId,//告知的对方clientId
					s => {
						console.log('tell agent ready to talk success', s);
					},
					error => {
						console.log('tell agent ready to talk failed', error);
					}
				);
	 */
	readyToTalk(user = {}, toWho, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		if (isEmptyString(this.participantId)) {
			onFailure(new IvcsError('5014', 'not start video'));
			return;
		}
		this.ccsClient.readyToTalk(this.participantId, user, toWho, (status, resp) => {
			Logger.info(`ivcs-js-sdk: ready to talk toWho ${toWho}, status:${status}, response:${JSON.stringify(resp)},time:${new Date().toISOString()}`);
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				this.islogs && this.islogs.isOpen && errorlogs(`ready-to-talk,participantId:${this.participantId},user:${user},toWho:${toWho} error:${resp}`, this.islogs, this.sessionId);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 通知机器人坐席,访客已经加入房间
	 * @param {object} user 自定义的一些个人信息（随路数据），如：头像，姓名，性别等
	 * @param {string} toWhom 发送给谁 clientId具体的用户 'customers'= 访客;'agents'= 坐席;'others'= 除了自己的所有人; 'all' = 所有人;
	 * @param {function} onSuccess 消息发送成功 'ok'
	 * @param {function} onFailure 消息发送失败 code:5013 = 发送失败即无法拉起机器人,需要应用层挂断会话;code:5014 = 未调用startVideo
	 * @example
	 * let user = {
						picture: localStorage.getItem('userPicture'),
						fullName: localStorage.getItem('fullName'),
						gender: localStorage.getItem('gender')
				   };
	   ivcs.client.noticeCustomerJoinRoom(
					user,//用户信息。非必填，可传{}
					agentClientId,//告知的对方clientId
					s => {
						console.log('noticeRobotCustomerJoinRoom success', s);
					},
					error => {
						console.log('noticeRobotCustomerJoinRoom failed', error);
					}
				);
	 */
	noticeCustomerJoinRoom(user, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		if (isEmptyString(this.participantId)) {
			onFailure(new IvcsError('5014', 'not start video'));
			return;
		}
		this.ccsClient.customerJoinRoom(this.participantId, user, toWhom, (status, resp, msgId) => {
			if (status === 'ok') {
				Logger.info(`ivcs-js-sdk:noticeCustomerJoinRoom success: user: ${JSON.stringify(user)},to ${toWhom},time:,${new Date().toISOString()}`);
				onSuccess('ok');
			} else {
				Logger.info(`ivcs-js-sdk:notice customer join room error participantId:${this.participantId},to ${toWhom} failed,reason:${resp},msgId ${msgId},time:${new Date().toISOString()}`);
				this.islogs && this.islogs.isOpen && errorlogs(`notice customer-join-room participantId:${this.participantId},to ${toWhom} error:${resp}`, this.islogs, this.sessionId);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 访客端实现前后摄像头切换，参考文档 {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia}
	 * IOS 采用 facingMode 方式切换
	 * Android 采用 enumerateDevices 方式切换
	 * 对于 Webview 前置摄像头取 设备列表第一个，后置摄像头取设备列表最后一个
	 * PC 采用遍历设备ID方式切换
	 * @since 1.0.4版本新增API
	 * @param { Function } onSuccess  切换成功，返回切换成功后得 mediaStream
	 * @param { Function } onFailure  切换失败，返回失败原因 {1023:切换流失败}
	 * @param { Function } onEnded  切换的画面视频帧结束，画面变黑，恢复则需要重新切换下摄像头。
	 * @param { string }   videoId 访客画面video标签的id，默认为空，不传则需要访客端自己去拿到mediaStream去渲染,支持传多个标签，格式'videoId1,videoId2'
	 * @param { Function } onplayError 切换后自动播放失败
	 * @example
	 * ivcs.client.switchCamera(
					success => {
						console.log('switch camera success',success);
					},
					err => {
						console.log('switch camera error',err);
					},
					onEnded => {
						console.log('switch camera onended',onEnded);
					},
					'customer',
					playerror = >{
						console.warning('customer video autoPlay error,need user interaction to trigger autoplay')
					}
				);
	 */
	switchCamera(onSuccess, onFailure, onEnded, videoId = '', onplayError) {
		this.stopLocalStream();
		this.cameraNavigator.switchCameraGetDeviceId(deviceId => {
			const audioDeviceId = null;
			const videoDeviceId = deviceId;
			const audioConstraints = this.mediaOptions.cameraStreamAudioConstraints;
			const videoConstraints = this.mediaOptions.cameraStreamVideoConstraints;
			this.cameraNavigator.createStream(
				this.irtcClient,
				audioDeviceId,
				videoDeviceId,
				audioConstraints,
				videoConstraints,
				stream => {
					this.irtcClient.switchCamera(this.publication, stream, onSuccess, onFailure);
					// this.publication
					// 	.updateMediaTrack(stream, Base.TrackKind.VIDEO)
					// 	.then(resp => {
					// 		Logger.info('ivcs-js-sdk:switchCamera success', resp, new Date().toISOString());
					// 		onSuccess(stream);
					// 	})
					// 	.catch(error => {
					// 		Logger.error('ivcs-js-sdk:switchCamera error', error, new Date().toISOString());
					// 		let err = new IvcsError('1023', error.message || error.name || error);
					// 		onFailure(err);
					// 	});
					if (videoId && onplayError) {
						const isMeStream = true;
						let videoIds = videoId.split(',');
						videoIds &&
							videoIds.forEach(item => {
								renderStreamWithVideoLabel(stream, item, isMeStream, onplayError, onEnded)
									.then()
									.catch(error => {
										onFailure(error);
									});
							});
					}
					if (stream && stream.mediaStream && stream.mediaStream.getVideoTracks()) {
						stream.mediaStream.getVideoTracks()[0].onended = () => {
							onEnded();
						};
					} else if (stream && stream.getVideoTracks()) {
						stream.getVideoTracks()[0].onended = () => {
							onEnded();
						};
					}
				},
				error => {
					this.islogs && this.islogs.isOpen && errorlogs('switchcamera error:', this.islogs, this.sessionId);
					this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
					onFailure(error);
				}
			);
		}, onFailure);
	}

	/**
	 * 获取访客地理位置信息
	 * @since 1.0.3版本新增API
	 * @param { function } onSuccess 获取成功回调
	 * @param { function } onError 获取失败回调
	 * @param { Number} mapTimeout 超时时间
	 * @example
	 * ivcs.client.requestMaplocation(
				resp => {
					console.log(resp);
				},
				error => {
					console.log(error);
				}
			);
	 */
	requestMaplocation(onSuccess, onError, mapTimeout) {
		if (!this.mapLocation) {
			let error = new IvcsError('80000', 'map init failed');
			onError(error);
			return;
		}
		this.mapLocation.getGaoDeLocation(onSuccess, onError, mapTimeout);
	}

	/**
	 * 收到对方音视频已准备好的消息
	 * @param {function} ack 收到消息的回调
	 * @example
	 * ivcs.client.onReadyToTalk(async resp => {
						console.log('agent video ready', resp);
		});
	 */
	onReadyToTalk(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.warning(`ivcs-js-sdk:onReadyToTalk error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onReadyToTalk(resp => {
			ack(resp);
		});
	}

	/**
	 * 访客会话被系统强制结束,网络异常导致服务结束
	 * @param {function} ack 返回具体的原因
	 * @example
	 * ivcs.client.onDrop(resp => {
				console.log('onDrop,,', resp);
			});
	 */
	onDrop(ack) {
		let dropMessage = {
			drop: {
				code: 401,
				message: 'connect forced termination drop',
			},
			error: {
				code: 402,
				message: 'socket io connect error',
			},
		};
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.warning(`ivcs-js-sdk:onDrop error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onDrop(resp => {
			releaseCcsClient(this.ccsClient);
			Logger.warning('ivcs-js-sdk:ccs connect forced termination drop', resp, new Date().toISOString());
			this.islogs && this.islogs.isOpen && errorlogs(`onDrop:${resp},message:${(dropMessage, drop)}`, this.islogs, this.sessionId);
			ack(dropMessage.drop);
		});
		this.ccsClient.onError(async data => {
			if (data && data.code === '1101') {
				//token expired
				let backendUrl = this.associatedUrls.backendurl;
				let newTokenTicket = await refreshNewToken(backendUrl, this.userId, this.ticket);
				this.ticket = newTokenTicket.ticket;
				this.accessToken = newTokenTicket.accessToken;
				// eslint-disable-next-line no-unused-vars
				let newConnect = await connect(this.ccsClient, this.ccsUrl, this.accessToken);
				// eslint-disable-next-line no-unused-vars
				let resumeConnect = resumeCcsConnection(this.ccsClient, this.ccsClient.ticket);
			} else {
				releaseCcsClient(this.ccsClient);
				this.islogs && this.islogs.isOpen && errorlogs(`onDrop onError:${data},message:${(dropMessage, error)}`, this.islogs, this.sessionId);
				ack(dropMessage.error);
			}
		});
	}

	/**
	 * 加入会话房间，在坐席加入会话房间后调用，只需要调用一次即可。
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 */
	async joinRoom(onSuccess, onFailure) {
		try {
			let turnServerUrl = this.associatedUrls.turnserverUrl;
			let turnServerCredential = this.associatedUrls.turnserverCredential;
			let turnServerUsername = this.associatedUrls.turnserverUsername;
			this.irtcClient = new IRtcClient(turnServerUrl, turnServerCredential, turnServerUsername);
			this.irtcClient.clientInfo.clientId = this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '';
			this.irtcClient.clientInfo.userName = this.customerName;
			this.irtcClient.clientInfo.role = 'customer';
			this.irtcClient.sessionId = deepCopy(this.sessionId);
			this.irtcClient.accessToken = deepCopy(this.accessToken);
			this.irtcClient.backendUrl = deepCopy(this.backendUrl);
			Logger.info(`ivcs-js-sdk:request-progress-start-join-room:${new Date().toISOString()}`);
			this.requestProgressLogs.push(`request-progress-start-join-room:${new Date().toISOString()}`);
			let videoLabel = 'customer' + this.ccsClient.clientId;
			this.sendStartVideoProgressMessage(videoLabel, 'customer', 'joining');
			let roomInfo = await joinRoom(this.irtcClient, this.roomId, this.roomToken);
			this.participantId = roomInfo.self.id;
			zmsAddEventListener(this.irtcClient);
			onSuccess(roomInfo);
			this.streamAddListener = addEventListener(IRTC_CUSTOMEREVENT.STREAMADD, info => {
				Logger.info('ivcs-js-sdk:new stream add with info:', info, ' at:', new Date().toISOString());
				if (info && info.data) {
					Logger.info('ivcs-js-sdk:new stream add with stream:', info.data, ' at:', new Date().toISOString());
					//添加asr的监听
					startAsrEvent(this.backendUrl, roomInfo.id, info.data.id, this.accessToken, this.participantId, 'baidu');
				}
			});
			this.streamAsrListener = addEventListener(IRTC_CUSTOMEREVENT.ASR, info => {
				Logger.info('ivcs-js-sdk:new stream asr event:', info, ' at:', new Date().toISOString());
				if (info && info.data) {
					Logger.info('ivcs-js-sdk:new stream asr event:', info.data, ' at:', new Date().toISOString());
					this.sendTextMessageWithFromClientId(
						info.data.content,
						info.data.clientId,
						'all',
						s => {
							Logger.info('ivcs-js-sdk:send asr text message success', s);
						},
						e => {
							Logger.info('ivcs-js-sdk:send asr text message error', e);
						}
					);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 推送本地音视频画面，在加入房间JoinRoom成功后调用。
	 * @param {string} videoLabel 视频标签的标识，可根据这个标识处理业务UI的布局
	 * @param {string} callMediaType 呼叫类型,可选值'audio','video'，默认是视频呼叫
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.startVideo(
					'customer',
					this.callMediaType,
					resp => {
						console.log('start video success', resp);
					},
					error => {
						console.log('start video failed', error);
					}
				);
	 */
	async startVideo(videoLabel, callMediaType = 'video', onSuccess, onFailure) {
		try {
			this.cameraNavigator = new Navigator();
			this.subscriptionStatIntervals = [];
			//set subscription data
			this.irtcClient ? (this.irtcClient.subscriptionList = new Map()) : '';
			this.onSessionRecordCrash(() => {});

			!this.irtcWhiteBoard ? (this.irtcWhiteBoard = new IrtcWhiteBoard(this.irtcClient)) : '';
			let streamOptions = {
				audioConstraints: this.mediaOptions.cameraStreamAudioConstraints,
				videoConstraints: this.mediaOptions.cameraStreamVideoConstraints,
				publishOptions: this.mediaOptions.cameraPublishOptions,
			};
			this.clientVideoLabel = deepCopy(videoLabel);
			this.callMediaType = deepCopy(callMediaType);
			Logger.info(`ivcs-js-sdk:request-progress-start-create-stream:${new Date().toISOString()}`);
			this.requestProgressLogs.push(`request-progress-start-create-stream:${new Date().toISOString()}`);
			let haveAudio = true;
			let haveVideo = this.callMediaType === 'video' ? true : false;
			let streamInfo = {
				participantId: this.participantId ? this.participantId : '',
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			};
			let userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				customerRole: this.customerRole,
				userName: this.customerName,
				role: 'customer',
			};
			this.sendStartVideoProgressMessage(videoLabel, 'customer', 'permission');
			this.localStream = await createCameraMicStream(this.irtcClient, streamOptions, videoLabel, haveAudio, haveVideo, streamInfo, userInfo);
			Logger.info(`ivcs-js-sdk:request-progress-start-publish-stream:${new Date().toISOString()}`);
			this.requestProgressLogs.push(`request-progress-start-publish-stream:${new Date().toISOString()}`);
			this.sendStartVideoProgressMessage(videoLabel, 'customer', 'publish');
			let publication = await publishStream(this.irtcClient, this.localStream, streamOptions.publishOptions);
			this.publication = publication;
			this.readyToTalkUserList.forEach(clientId => {
				let user = { streamId: publication?.id };
				this.readyToTalk(
					user,
					clientId,
					() => {
						Logger.info(`ivcs-js-sdk:ready-to-talk success,user:${JSON.stringify(user)} clientId:${clientId},at:${new Date().toISOString()}`);
					},
					() => {
						Logger.info(`ivcs-js-sdk:ready-to-talk error,user:${JSON.stringify(user)}, clientId:${clientId},at:${new Date().toISOString()}`);
					}
				);
			});
			publicationAddEventListener(this.irtcClient, publication);
			this.ccsClient.sendZmsRoomEvent({ event: 'publish-success' }, (s, r, m) => {
				Logger.info(`ivcs-js-sdk:send zms-room-event publish-success event with status:${s},response:${r},msgId:${m},at:${new Date().toISOString()}`);
			});
			this.ccsClient && this.ccsClient.noticeStreamId({ streamId: publication.id });
			this.publicationStatIntervals = [];
			this.getPublicationStats(publication);
			// eslint-disable-next-line no-unused-vars
			Logger.info(`ivcs-js-sdk:request-progress-start-add-to-mix:${new Date().toISOString()}`);
			this.requestProgressLogs.push(`request-progress-start-add-to-mix:${new Date().toISOString()}`);
			await addStreamToMixStream(this.backendUrl, this.roomId, publication.id, this.accessToken);
			this.localPublicationId = publication.id;
			onSuccess({
				publicationId: publication.id,
				// roomInfo: roomInfo,
				localStream: this.localStream,
			});
		} catch (e) {
			if (this.localStream) {
				stopMediaStream(this.localStream);
			}
			onFailure(e);
			this.islogs && this.islogs.isOpen && errorlogs(`startVideo error videoID:${videoLabel} `, this.islogs, this.sessionId);
			this.islogs && this.islogs.isOpen && errorlogs(e, this.islogs, this.sessionId);
		}
	}

	/**
	 * 发送开启音视频的进度自定义消息
	 * @ignore
	 * @param {string} label 视频标签的标识，可根据这个标识处理业务UI的布局
	 * @param {string} role 'customer' | 'agent'
	 * @param {string} status 'permission' | 'publish'
	 */
	sendStartVideoProgressMessage(label, role, status) {
		const progressData = {
			label: label,
			clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			role: role || 'customer',
			isRobotStream: false,
			status: status,
			businessType: 'notice_media_progress',
			customerRole: this.customerRole,
			userName: this.customerName,
		};
		this.sendCustomMessage(
			progressData,
			'others',
			s => {
				Logger.info(`ivcs-js-sdk:sendCustomMessage notice start-video progress success, with status:${status}`);
			},
			e => {
				Logger.error(`ivcs-js-sdk:ivcs-js-sdk:sendCustomMessage notice start-video progress error:${e}, with status:${status}`);
			}
		);
	}

	/**
	 * 获取推流的stats
	 * @ignore
	 * @param {object} publication
	 */
	getPublicationStats(publication) {
		if (this.islogs && this.islogs.isOpen) {
			const pubIntervalTime = 2000;
			let interval = setInterval(() => {
				publication.getStats().then(
					stats => {
						for (let [key, value] of stats) {
							let msg = `publication-getstats key=${key},${JSON.stringify(value)}`;
							this.islogs && this.islogs.isOpen && this.islogs.isOpenStats && errorlogs(msg, this.islogs, this.sessionId);
						}
					},
					err => {
						Logger.info(`ivcs-js-sdk:${publication.id} getStats failed: `, err);
						clearInterval(interval);
					}
				);
			}, pubIntervalTime);
			this.publicationStatIntervals.push(interval);
		}
	}

	/**
	 * 获取拉流的stats
	 * @ignore
	 * @param {object} subscription
	 * @param {string} streamLabel 需要渲染的视频标签
	 * @param {string} videoId 被渲染的video标签的id
	 */
	getSubscriptionStats(subscription, streamLabel, videoId) {
		if (this.islogs && this.islogs.isOpen) {
			const subIntervalTime = 2000;
			let interval = setInterval(() => {
				subscription.getStats().then(
					stats => {
						for (let [key, value] of stats) {
							let msg = `subscription-getstats-streamlabel-${streamLabel},videoId=${videoId},key=${key},${JSON.stringify(value)}`;
							this.islogs && this.islogs.isOpen && this.islogs.isOpenStats && errorlogs(msg, this.islogs, this.sessionId);
						}
					},
					err => {
						Logger.info(`ivcs-js-sdk:${subscription.id} getStats failed: `, err);
						clearInterval(interval);
					}
				);
			}, subIntervalTime);
			this.subscriptionStatIntervals.push(interval);
		}
	}

	/**
	 * 挂断会话,清除房间里面的推拉流监听,清除临时变量
	 * @ignore
	 */
	clearSessionData() {
		for (let i = 0; i < this.subscriptionStatIntervals ? this.subscriptionStatIntervals.length : 0; i++) {
			clearInterval(this.subscriptionStatIntervals[i]);
		}
		this.subscriptionStatIntervals = [];
		for (let i = 0; i < this.publicationStatIntervals ? this.publicationStatIntervals.length : 0; i++) {
			clearInterval(this.publicationStatIntervals[i]);
		}
		this.publicationStatIntervals = [];
		this.irtcClient = null;
		clearInterval(this.refreshTokenInterval);
	}

	/**
	 * 停止本地音视频流
	 */
	stopLocalMedia() {
		if (this.localStream) {
			stopMediaStream(this.localStream);
		} else {
			Logger.error('ivcs-js-sdk:stopLocalMedia error no localStream ', new Date().toISOString());
		}
		if (this.publication) {
			this.irtcClient.publicationRemoveListener(this.publication);
			this.publication.stop();
		} else {
			Logger.error('ivcs-js-sdk:stopLocalMedia error no publish stream ', new Date().toISOString());
		}
	}

	/**
	 * 重新恢复本地音视频流
	 * @param {string} mediaType audio | video
	 * @param {function} onSuccess LocalStream 可以直接渲染到video标签上
	 * @param {function} onFailure 操作失败
	 * @param {string} videoId 访客画面video标签的id，默认为空，不传则需要访客端自己去拿到LocalStream去渲染,支持传多个标签，格式'videoId1,videoId2'
	 */
	async restoreLocalMedia(mediaType = 'video', onSuccess, onFailure, videoId) {
		try {
			stopMediaStream(this.localStream);
			let streamOptions = {
				audioConstraints: this.mediaOptions.cameraStreamAudioConstraints,
				videoConstraints: this.mediaOptions.cameraStreamVideoConstraints,
				publishOptions: this.mediaOptions.cameraPublishOptions,
			};
			let haveAudio = true;
			let haveVideo = mediaType === 'video' ? true : false;
			let streamInfo = {
				participantId: this.participantId ? this.participantId : '',
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			};
			let userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				customerRole: this.customerRole,
				userName: this.customerName,
				role: 'customer',
			};
			this.localStream = await createCameraMicStream(this.irtcClient, streamOptions, this.clientVideoLabel, haveAudio, haveVideo, streamInfo, userInfo);
			this.publication = await publishStream(this.irtcClient, this.localStream, streamOptions.publishOptions);
			this.ccsClient && this.ccsClient.noticeStreamId({ streamId: this.publication.id });
			this.getPublicationStats(this.publication);
			this.localPublicationId = this.publication.id;
			// eslint-disable-next-line no-unused-vars
			let addStreamToMix = await addStreamToMixStream(this.backendUrl, this.roomId, this.publication.id, this.accessToken);
			if (videoId) {
				const isMeStream = true;
				let videoIds = videoId.split(',');
				videoIds &&
					videoIds.forEach(item => {
						renderStreamWithVideoLabel(
							this.localStream,
							item,
							isMeStream,
							() => {},
							() => {}
						)
							.then()
							.catch(error => {
								Logger.info(`ivcs-js-sdk:restoreLocalMedia renderStreamWithVideoLabel label,${item} error:${error}',${new Date().toISOString()}`);
							});
					});
			}
			onSuccess(this.localStream);
		} catch (error) {
			Logger.error('ivcs-js-sdk:restoreLocalMedia error', error);
			let response = new IvcsError(error.code || '1023', error.name || error.message || error);
			this.islogs && this.islogs.isOpen && errorlogs('restoreLocalMedia error:' + error.name || error.message || error, this.islogs, this.sessionId);
			onFailure(response);
		}
	}

	/**
	 * 恢复音视频推送
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.restoreVideo((s)=>{
				console.log('restore video success',s);
			},(e)=>{
				console.log('restore video error',e)
			})
	 */
	async restoreVideo(onSuccess, onFailure) {
		try {
			stopMediaStream(this.localStream);
			let streamOptions = {
				audioConstraints: this.mediaOptions.cameraStreamAudioConstraints,
				videoConstraints: this.mediaOptions.cameraStreamVideoConstraints,
				publishOptions: this.mediaOptions.cameraPublishOptions,
			};
			let haveAudio = true;
			let haveVideo = this.callMediaType === 'video' ? true : false;
			let streamInfo = {
				participantId: this.participantId ? this.participantId : '',
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
			};
			let userInfo = {
				clientId: this.ccsClient && this.ccsClient.clientId ? this.ccsClient.clientId : '',
				customerRole: this.customerRole,
				userName: this.customerName,
				role: 'customer',
			};
			this.localStream = await createCameraMicStream(this.irtcClient, streamOptions, this.clientVideoLabel, haveAudio, haveVideo, streamInfo, userInfo);
			this.publication = await publishStream(this.irtcClient, this.localStream, streamOptions.publishOptions);
			this.ccsClient && this.ccsClient.noticeStreamId({ streamId: this.publication.id });
			this.getPublicationStats(this.publication);
			this.localPublicationId = this.publication.id;
			// eslint-disable-next-line no-unused-vars
			let addStreamToMix = await addStreamToMixStream(this.backendUrl, this.roomId, this.publication.id, this.accessToken);
			onSuccess({
				publicationId: this.publication.id,
				localStream: this.localStream,
			});
		} catch (error) {
			Logger.error('ivcs-js-sdk:restore video error', error);
			let response = new IvcsError(error.code || '1023', error.name || error.message || error);
			this.islogs && this.islogs.isOpen && errorlogs('restore video error:' + error.name || error.message || error, this.islogs, this.sessionId);
			onFailure(response);
		}
	}

	/**
	 * 语音升级到视频通话
	 * @param {function} onSuccess 成功 {stream}  可用于渲染画面
	 * @param {function} onFailure 失败 {1022:未开启音视频通话,1023:updateMedia升级失败，其他错误参考错误码}
	 * @param {string} videoId 访客画面video标签的id，默认为空，不传则需要访客端自己去拿到mediaStream去渲染,支持传多个标签，格式'videoId1,videoId2'
	 * @example
	 * ivcs.client.voiceToVideo(
				resp => {
					console.log('voiceToVideo success', resp);
				},
				error => {
					console.log('voiceToVideo error', error);
				}
			);
	 */
	async voiceToVideo(onSuccess, onFailure, videoId) {
		if (!this.publication) {
			Logger.warning(`ivcs-js-sdk:no audio or video calls`);
			let error = new IvcsError('1022', 'No audio or video calls');
			onFailure(error);
			return;
		}
		let constraints = {
			audio: this.mediaOptions.cameraStreamAudioConstraints,
			video: this.mediaOptions.cameraStreamVideoConstraints,
		};
		try {
			let stream = await this.irtcClient.createAudioMediaStream(constraints, {}, false);
			this.publication
				.updateMedia(stream)
				.then(resp => {
					Logger.info('ivcs-js-sdk:voice to video success', resp, new Date().toISOString());
					if (videoId) {
						const isMeStream = true;
						let videoIds = videoId.split(',');
						videoIds &&
							videoIds.forEach(item => {
								renderStreamWithVideoLabel(
									stream,
									item,
									isMeStream,
									() => {},
									() => {}
								)
									.then()
									.catch(error => {
										Logger.info(`ivcs-js-sdk:voice to video renderStreamWithVideoLabel label,${item} error:${error}',${new Date().toISOString()}`);
									});
							});
					}
					onSuccess(stream);
				})
				.catch(error => {
					Logger.error('ivcs-js-sdk:voice to video error', error, new Date().toISOString());
					let err = new IvcsError('1023', error.message || error.name || error);
					this.islogs && this.islogs.isOpen && errorlogs('voice to video updateMedia error ', this.islogs, this.sessionId);
					this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
					onFailure(err);
				});
		} catch (error) {
			Logger.error('ivcs-js-sdk:voice to video catch error', error, new Date().toISOString());
			let err = new IvcsError(error.code || '1023', error.message || error.name || error);
			this.islogs && this.islogs.isOpen && errorlogs('voice to video catch error ', this.islogs, this.sessionId);
			this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
			onFailure(err);
		}
	}

	/**
	 * 视频降级成音频通话
	 * @param {function} onSuccess 成功 {stream}  可用于渲染画面
	 * @param {function} onFailure 失败 {1022:未开启音视频通话,1023:updateMedia升级失败，其他错误参考错误码}
	 * @param {string} videoId 访客画面video标签的id，默认为空，不传则需要访客端自己去拿到mediaStream去渲染,支持传多个标签，格式'videoId1,videoId2'
	 * @example
	 * ivcs.client.videoToVoice(
				resp => {
					console.log('videoToVoice success', resp);
				},
				error => {
					console.log('videoToVoice error', error);
				}
			);
	 */
	async videoToVoice(onSuccess, onFailure, videoId) {
		if (!this.publication) {
			Logger.warning(`ivcs-js-sdk:no audio or video calls`);
			let error = new IvcsError('1022', 'No audio or video calls');
			onFailure(error);
			return;
		}
		let constraints = {
			audio: this.mediaOptions.cameraStreamAudioConstraints,
			video: false,
		};
		try {
			let stream = await this.irtcClient.createVideoMediaStream(constraints, {}, false);
			this.publication
				.updateMedia(stream)
				.then(resp => {
					Logger.info('ivcs-js-sdk:video to voice success', resp, new Date().toISOString());
					if (videoId) {
						const isMeStream = true;
						let videoIds = videoId.split(',');
						videoIds &&
							videoIds.forEach(item => {
								renderStreamWithVideoLabel(
									stream,
									item,
									isMeStream,
									() => {},
									() => {}
								)
									.then()
									.catch(error => {
										Logger.info(`ivcs-js-sdk:video to voice renderStreamWithVideoLabel label,${item} error:${error}',${new Date().toISOString()}`);
									});
							});
					}
					onSuccess(stream);
				})
				.catch(error => {
					Logger.error('ivcs-js-sdk:video to voice error', error, new Date().toISOString());
					let err = new IvcsError('1023', error.message || error.name || error);
					this.islogs && this.islogs.isOpen && errorlogs('video to voice updateMedia error ', this.islogs, this.sessionId);
					this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
					onFailure(err);
				});
		} catch (error) {
			Logger.error('ivcs-js-sdk:video to voice catch error', error, new Date().toISOString());
			let err = new IvcsError(error.code || '1023', error.message || error.name || error);
			this.islogs && this.islogs.isOpen && errorlogs('video to voice catch error ', this.islogs, this.sessionId);
			this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
			onFailure(err);
		}
	}

	/**
	 * 渲染视频流,一般用在监听有流加入房间后。
	 * @param {string} streamLabel 需要渲染的视频标签
	 * @param {string} videoId 被渲染的video标签的id
	 * @param {function} onSuccess 渲染成功 ,返回不为空则表示被渲染的流的subscriptionId<后面可根据这个subscriptionId重新渲染画面>,返回空则表示是被渲染的是自己本地的画面
	 * @param {function} onFailure 失败
	 * @param {function} onPlayError video播放视频失败,需要用户交互触发播放,如:let video = document.getElementById(videoId);video && video.play();
	 * @param {function} onEnded 画面的视频帧结束,返回video标签的id，业务可根据video标签id，给到不同的处理逻辑，自己的画面标签id,可通过切换摄像头恢复，对方的画面标签id,可重新订阅(renderStream)恢复
	 * @example
	 *  this.streamAddEvent = ivcs.client.addEventListener('onStreamAdd', streamInfo => {
		  let label = streamInfo.data.streamLabel;
		});
		ivcs.client.renderStream(
						label,//需要渲染的视频标签
						label,//被渲染的video标签的id
						resp => {
							console.log('render success', resp, label, label);
						},
						error => {
							console.log('render error', error);
						},
						playError => {
							console.log('render play error', playError);
							this.userGesturePlayVideo(label);
						},
						onEnded => {
							console.log('render onEnded', onEnded);
						}
					);
	 */
	async renderStream(streamLabel, videoId, onSuccess, onFailure, onPlayError, onEnded) {
		Logger.info(`ivcs-js-sdk:start-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
		this.requestProgressLogs.push(`start-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
		renderRemoteStream(
			this.irtcClient,
			streamLabel,
			videoId,
			(resp, subscription, streamInfo) => {
				if (subscription) {
					this.getSubscriptionStats(subscription, streamLabel, videoId);
					this.irtcClient.subscriptionList.set(streamLabel, subscription);
				}
				Logger.info(`ivcs-js-sdk:success-render-stream:,streamLabel:${streamLabel},videoId:${videoId},streamInfo:${JSON.stringify(streamInfo)},publicationId:${this.publication?.id},at:${new Date().toISOString()}`);
				this.requestProgressLogs.push(`success-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				onSuccess(resp);
				if (streamInfo?.userInfo?.clientId && this.publication?.id) {
					let user = { streamId: this.publication?.id };
					this.readyToTalk(
						user,
						streamInfo.userInfo.clientId,
						() => {
							Logger.info(`ivcs-js-sdk:ready-to-talk success,user:${JSON.stringify(user)},streamLabel:${streamLabel},videoId:${videoId}, clientId:${streamInfo.userInfo.clientId},streamInfo:${JSON.stringify(streamInfo)},at:${new Date().toISOString()}`);
						},
						() => {
							Logger.info(`ivcs-js-sdk:ready-to-talk error,user:${JSON.stringify(user)},streamLabel:${streamLabel},videoId:${videoId}, clientId:${streamInfo.userInfo.clientId},streamInfo:${JSON.stringify(streamInfo)},at:${new Date().toISOString()}`);
						}
					);
				} else if (streamInfo?.userInfo?.clientId) {
					this.readyToTalkUserList.push(streamInfo.userInfo.clientId);
				}
			},
			error => {
				Logger.info(`ivcs-js-sdk:failed-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				this.requestProgressLogs.push(`failed-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				this.islogs && this.islogs.isOpen && errorlogs(error, this.islogs, this.sessionId);
				onFailure(error);
			},
			error => {
				Logger.info(`ivcs-js-sdk:autoplay-error-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				this.requestProgressLogs.push(`autoplay-error-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				onPlayError(error);
			},
			videoId => {
				Logger.info(`ivcs-js-sdk:onended-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				this.requestProgressLogs.push(`onended-render-stream:,streamLabel:${streamLabel},videoId:${videoId},at:${new Date().toISOString()}`);
				onEnded(videoId);
			}
		);
	}

	/**
	 * 重新渲染一次远端流，用于修复iphone version17+ 浏览器无法最大化显示
	 * @ignore
	 * @param {string} streamLabel
	 * @param {string} videoId
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 * @param {function} onPlayError
	 * @param {function} onEnded
	 */
	renderRemoteStreamWithLabelAgain(streamLabel, videoId, onSuccess, onFailure, onPlayError, onEnded) {
		renderStreamAgain(this.irtcClient, streamLabel, videoId, onSuccess, onFailure, onPlayError, onEnded);
	}

	/**
	 * 停止渲染,主要用于订阅机器人后，机器人挂断后，取消订阅机器人。
	 * @since 1.0.4版本新增API
	 * @param {string} subscriptionId 渲染视频流成功后返回的id
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.stopRenderStream(
			subscriptionId,
			s => {
				console.log('stop render stream success', subscriptionId, s);
			},
			e => {
				console.log('stop render stream error', subscriptionId, e);
			}
		);
	 */
	stopRenderStream(subscriptionId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('1024', 'irtcClient is invalid'));
			return;
		}
		let subscribes = this.irtcClient.subscriptionArr ? this.irtcClient.subscriptionArr : [];
		for (let i = 0; i < subscribes.length; i++) {
			let subscription = subscribes[i];
			if (subscriptionId === subscription.id) {
				subscription.stop();
				onSuccess('ok');
				return;
			}
		}
		onFailure(new IvcsError('1024', `subscriptionId, ${subscriptionId} is invalid`));
	}

	/**
	 * 监听tts辅助播报语音开始播报
	 * @param {function} ack {streamId:string(tts语音流id),streamLabel:string(tts语音流标签),userInfo:object(开始tts播报的坐席个人信息),content:string(播报的文本内容)}
	 * @example
	 * ivcs.client.onTTSStreamSpeaking((info)=>{});
	 */
	onTTSStreamSpeaking(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.ttsStreamSpeaking, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听tts辅助播报语音播报结束
	 * @param {function} ack {streamId:string(tts语音流id),streamLabel:string(tts语音流标签),userInfo:object(开始tts播报的坐席个人信息),content:string('ok')}
	 * @example
	 * ivcs.client.onTTSStreamSpeakDone((info)=>{});
	 */
	onTTSStreamSpeakDone(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.ttsStreamSpeakDone, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听通话中那个流正在说话
	 * @param {function} ack {streamId:string(流id),streamLabel:string(流标签),userInfo:object(), audioOnly:boolean()}
	 * @example
	 * ivcs.client.onActiveAudioInputStream((info)=>{});
	 */
	onActiveAudioInputStream(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.activeAudioInputStream, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听会话录像中断事件。
	 * @param {function} ack {recordingId: string(录制id)}
	 * @ignore
	 * @example
	 * ivcs.client.onSessionRecordCrash((data)=>{console.log(data.recordingId)});
	 */
	onSessionRecordCrash(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.sessionOnEnd, result => {
			const data = result.data;
			Logger.info(`ivcs-js-sdk:zms sessionOnEnd, data: ${JSON.stringify(data)},systemRecordId:${this.systemRecordId},time:${new Date().toISOString()}`);
			if (data && data.sessionId === this.systemRecordId && this.irtcClient) {
				this.irtcClient.updateRecordCrashEvent(this.systemRecordId);
				ack(data);
			}
		});
		this.addEventListener(IRTC_CUSTOMEREVENT.sessionOnError, result => {
			const data = result.data;
			Logger.info(`ivcs-js-sdk:zms sessionOnError, data: ${JSON.stringify(data)},systemRecordId:${this.systemRecordId},time:${new Date().toISOString()}`);
			if (data && data.sessionId === this.systemRecordId && this.irtcClient) {
				this.irtcClient.updateRecordCrashEvent(this.systemRecordId);
				ack(data);
			}
		});
	}

	/**
	 * 监听会话录像中断事件。
	 * @param {function} ack {recordingId: string(录制id)}
	 * @ignore
	 * @example
	 * ivcs.client.onSessionRecordCrash((data)=>{console.log(data.recordingId)});
	 */
	onSessionRecordCrash(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.sessionOnEnd, result => {
			const data = result.data;
			Logger.info(`ivcs-js-sdk:zms sessionOnEnd, data: ${JSON.stringify(data)},systemRecordId:${this.systemRecordId},time:${new Date().toISOString()}`);
			if (data && data.sessionId === this.systemRecordId && this.irtcClient) {
				this.irtcClient.updateRecordCrashEvent(this.systemRecordId);
				ack && ack(data);
			}
		});
		this.addEventListener(IRTC_CUSTOMEREVENT.sessionOnError, result => {
			const data = result.data;
			Logger.info(`ivcs-js-sdk:zms sessionOnError, data: ${JSON.stringify(data)},systemRecordId:${this.systemRecordId},time:${new Date().toISOString()}`);
			if (data && data.sessionId === this.systemRecordId && this.irtcClient) {
				this.irtcClient.updateRecordCrashEvent(this.systemRecordId);
				ack && ack(data);
			}
		});
	}

	/**
	 * 监听访客端本地推送画面的媒体连接状态。
	 * @param {function} ack {streamLabel:string(本地推流标签),userInfo:object(该画面包含的client信息),publishStatus:{status:string('failed'|'connecting'|'connected'|'disconnected'|'quality-report'),data:object(网络状况信息)}}
	 * @example
	 * ivcs.client.onPublishMediaStatus((info)=>{});
	 */
	onPublishMediaStatus(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.publishStreamStatus, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 监听渲染的远端画面的媒体连接状态。
	 * @param {function} ack {streamLabel:string(渲染的流标签),userInfo:object(该画面包含的client信息),renderStatus:{status:string('failed'|'connecting'|'connected'|'disconnected'|'quality-report'),data:object(网络状况信息)}}
	 * @example
	 * ivcs.client.onPublishMediaStatus((info)=>{});
	 */
	onRenderMediaStatus(ack) {
		this.addEventListener(IRTC_CUSTOMEREVENT.remoteStreamStatus, result => {
			const data = result.data;
			ack(data);
		});
	}

	/**
	 * 根据订阅的流标签，取消订阅
	 * @param {string} streamLabel 订阅过的远端流label
	 * @param {function} onSuccess 成功 'ok'
	 * @param {function} onFailure 失败 {code:'1024'|'1025'|'1026',message} 1024=无音视频会话,1025=暂无订阅任何画面,1026=远端流暂未订阅
	 * @example
	 * ivcs.client.stopRenderStreamWithLabel(
			'screen',
			s => {
				console.log('stopRenderStreamWithLabel success', s);
			},
			e => {
				console.log('stopRenderStreamWithLabel error', e);
			}
		);
	 */
	stopRenderStreamWithLabel(streamLabel, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('1024', 'irtcClient is invalid'));
			return;
		}
		if (!this.irtcClient.subscriptionList) {
			onFailure(new IvcsError('1025', 'subscriptionList is empty'));
			return;
		}
		let subscription = this.irtcClient.subscriptionList.get(streamLabel);
		if (subscription) {
			subscription.stop();
			onSuccess('ok');
		} else {
			onFailure(new IvcsError('1026', `streamLabel,${streamLabel} is invalid,not subscribe with streamLabel:${streamLabel}`));
		}
	}

	/**
	 * 发送文字消息
	 * @param {string} message 消息内容
	 * @param {string} clientId 接受消息的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';或者具体用户的clientId
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.sendTextMessage(
				message,
				undefined,
				resp => {
					console.log('send success', resp);
				},
				error => {
					console.log('send error', error);
				}
			);
	 */
	sendTextMessage(message, clientId = 'agents', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendTextMessage(message, clientId, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess();
			} else {
				Logger.info(`ivcs-js-sdk:send text message:${message} failed,reason:${resp},msgId${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到文字消息
	 * @param {function} ack 具体的消息数据
	 * @example
	 * 	ivcs.client.onTextMessage(resp => {
			console.log('ontextmessage',resp);
		});
	 */
	onTextMessage(ack) {
		onTextMessage(this.ccsClient, resp => {
			ack(resp);
		});
	}

	/**
	 * 移除会话中的事件监听
	 * @param {string|array} event 事件名或者集合 如'text',['text','link']
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * let event = 'text';
	   ivcs.client.removeSessionEventListener(event,(s)=>{
				console.log(`remove event:${event} success,${s}`)
			},(e)=>{
				console.log(`remove event:${event} error,${e}`)
			})
	 */
	removeSessionEventListener(event, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.removeListener(event);
		onSuccess('ok');
	}

	/**
	 * 发送资源文件
	 * @param {string} type 文件类型 支持:'doc', 'pic', 'video', 'audio', 'page', 'paper'
	 * @param {FormData} formData  具体文件的数据
	 * @param {string} clientId 接受文件的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';或者具体用户的clientId
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.shareResource(
				'pic',
				 data,
				'others',
				resp => {
					console.log('shareResource resp', resp);
				},
				error => {
					console.log('send pic Message failed', error);
				}
			);
	 */
	async shareResource(type, formData, clientId = 'agents', onSuccess, onFailure) {
		let supportType = ['doc', 'pic', 'video', 'audio', 'page', 'paper'];
		if (supportType.indexOf(type) === -1) {
			let error = new IvcsError('2001', `type is invalid,only support 'doc','pic', 'video', 'audio', 'page', 'paper'`);
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		try {
			let url = this.backendUrl;
			let uploadResult = await uploadFile(url, formData, this.accessToken);
			this.ccsClient.shareResource(type, uploadResult.url, uploadResult.originalname, clientId, (status, resp, msgId) => {
				if (status === 'ok') {
					onSuccess(uploadResult);
				} else {
					Logger.info(`ivcs-js-sdk:send shareResource formdata: ${formData} failed,reason:${resp},msgId${msgId},${new Date().toISOString()}`);
					dealWithCcsSendMessageResponse(status, resp, onFailure);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 根据文件的url发送文件。
	 * @param {string} type 文件类型 支持:'doc', 'pic', 'video', 'audio', 'page', 'paper'
	 * @param {string} url 文件相应的url地址
	 * @param {string} toRole 接受文件的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';
	 * @param {string} fileName 文件名
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.shareResourceWithUrl(
				'pic',
				 url,
				'others',
				'fileName'
				resp => {
					console.log('shareResourceWithFileInfo resp', resp);
				},
				error => {
					console.log('send pic Message failed', error);
				}
			);
	 */
	async shareResourceWithUrl(type, url, toRole = 'agents', fileName = '', onSuccess, onFailure) {
		let supportType = ['doc', 'pic', 'video', 'audio', 'page', 'paper'];
		if (supportType.indexOf(type) === -1) {
			let error = new IvcsError('2001', `type is invalid,only support 'doc','pic', 'video', 'audio', 'page', 'paper'`);
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		try {
			this.ccsClient.shareResource(type, url, fileName, toRole, (status, resp, msgId) => {
				if (status === 'ok') {
					onSuccess();
				} else {
					Logger.info(`ivcs-js-sdk:send shareResource message failed,${resp},msgId${msgId},${new Date().toISOString()}`);
					dealWithCcsSendMessageResponse(status, resp, onFailure);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 根据文件详情发送文件资源<主要用于IM里面发送文件图片等>
	 * @param {string} type 文件类型 支持:'doc', 'pic', 'video', 'audio', 'page', 'paper'
	 * @param {object} fileData 文件对象 { name: 文件名称(String), size: 文件大小(Number), url: 文件地址(String) }
	 * @param {string} toRole 接受文件的用户，'customers'= 访客;'agents'= 坐席;'all'= 所有人，'others'= 除了自己，所有人,  默认是'others';
	 * @param {function} onSuccess 成功回调
	 * @param {function} onFailure 失败回调
	 * @example
	 * ivcs.client.shareResourceWithFileInfo(
				'pic',
				data,
				'others',
				resp => {
					console.log('shareResourceWithFileInfo resp', resp);
				},
				error => {
					console.log('send pic Message failed', error);
				}
			);
	 */
	async shareResourceWithFileInfo(type, fileData, toRole = 'others', onSuccess, onFailure) {
		let supportType = ['doc', 'pic', 'video', 'audio', 'page', 'paper'];
		if (supportType.indexOf(type) === -1) {
			let error = new IvcsError('2001', `type is invalid,only support 'doc','pic', 'video', 'audio', 'page', 'paper'`);
			onFailure(error);
			return;
		}
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}

		try {
			this.ccsClient.shareResourceWithFileInfo(type, fileData, toRole, (status, resp, msgId) => {
				if (status === 'ok') {
					onSuccess();
				} else {
					Logger.info(`ivcs-js-sdk:send shareResourceWithFileInfo message failed,${resp},msgId${msgId},${new Date().toISOString()}`);
					dealWithCcsSendMessageResponse(status, resp, onFailure);
				}
			});
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 收到资源文件信息（图片，文档等）
	 * @param {function} ack 返回具体的资源文件数据
	 * @example
	 * ivcs.client.onResourceMessage(resp => {
			console.log('receive resource message,', resp);
		});
	 */
	onResourceMessage(ack) {
		onResourceMessage(this.ccsClient, resp => {
			ack(resp);
		});
	}

	/**
	 * 监听坐席发送静默等待的消息
	 * @param {function} ack 返回消息的内容
	 * @example
	 * ivcs.client.onSilentWaiting(data => {
			console.log('onSilentWaiting', data);
		});
	 */
	onSilentWaiting(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.warning(`ivcs-js-sdk:onSilentWaiting error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onSilentWaiting(data => {
			ack(data);
		});
	}

	/**
	 * 收到坐席正在转接中的消息，坐席转接给其他坐席触发该事件。
	 * @param {function} ack 事件回调 data 转接的相关信息{status:'start'|'end'} // start转接开始，end转接结束 
	 * @example
	 * ivcs.client.onHoldOn(data => {
				console.log('onhold on message,', data, new Date());
				this.isTransfer = true;
			});
	 */
	onHoldOn(ack) {
		onHoldOn(this.ccsClient, resp => {
			ack(resp);
		});
	}

	/**
	 * 发送自定义消息
	 * @param {string|object} data 自定义的消息内容
	 * @param {string} toRoles 发送消息的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.sendCustomMessage(
				data,
				role,
				success => {
					console.log('发送消息成功！' + JSON.stringify(data), success);
				},
				error => {
					console.log('发送消息失败！' + JSON.stringify(data), error);
				}
			);
	 */
	sendCustomMessage(data, toRoles = 'agents', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		this.ccsClient.sendCustomMessage(data, toRoles, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess('ok');
			} else {
				Logger.info(`ivcs-js-sdk:send custom message: ${data} to ${toRoles} failed,reason:${resp},msgId ${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到自定义消息
	 * @param {function} ack 返回具体的消息内容
	 * @example
	 * ivcs.client.onCustomMessage(resp => {
				if (resp.data.businessType == 'mark_trace') {
					let code = resp.data.message.track;
					console.log('startScreen mark code', code);
				}
				...
			});
	 */
	onCustomMessage(ack) {
		onCustomMessage(this.ccsClient, data => {
			ack(data);
		});
	}

	/**
	 * 对自己画面的暂停(停止数据传输)，支持音频/视频，暂停视频，则对方看不到自己实时画面;暂停音频，则对方听不到自己的实时声音
	 * @param {string} type  可选值 'video','audio'
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.mute(
			'audio',
			function() {
				console.log('mute audio succss');
			},
			function(error) {
				console.log('mute audio failed', error);
			}
		);
	 */
	async mute(type, onSuccess, onFailure) {
		try {
			await mute(this.localPublicationId, type, this.irtcClient);
			onSuccess();
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 自己画面的恢复(恢复数据传输)，支持音频/视频，恢复视频，则对方可以继续看到自己实时画面;恢复音频，则对方可以继续听到自己的实时声音
	 * @param {string} type 可选值 'video','audio'
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.unmute(
			'audio',
			function() {
				console.log('unmute audio succss');
			},
			function(error) {
				console.log('unmute audio failed', error);
			}
		);
	 */
	async unmute(type, onSuccess, onFailure) {
		try {
			await unmute(this.localPublicationId, type, this.irtcClient);
			onSuccess();
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 收到邀请屏幕共享消息
	 * @param {function} ack 事件消息回调
	 * @example
	 * ivcs.client.onRequestScreenShare(data => {
		});
	 */
	onRequestScreenShare(ack) {
		receiveRequestScreenShareMessage(this.ccsClient, resp => {
			ack(resp);
		});
	}

	/**
	 * 发送确认消息，告知对方相应的确认信息
	 * @param {string|object} message 确认消息的具体内容
	 * @param {string} toRole 发送消息的用户，'customers'= 访客;'agents'= 坐席;'all'= 除了自己的所有人，默认是'agents';
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 *  let data = 'ok';
		let id = clientId ? clientId : 'agents'; //判断clientId是否存在
		ivcs.client.sendMessageConfirmation(
			data,
			id,
			resp => {
				console.log('send confirmation message success', resp);
			},
			error => {
				console.log('send confirmation message error', error);
			}
		);
	 */
	sendMessageConfirmation(message, toRole = 'agents', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		// eslint-disable-next-line no-unused-vars
		this.ccsClient.sendMessageConfirmation(message, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess();
			} else {
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到信息确认消息
	 * @param {function} ack 返回信息确认的具体数据
	 * @example
	 * ivcs.client.onMessageConfirmation(data => {
		});
	 */
	onMessageConfirmation(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.warning(`ivcs-js-sdk:onMessageConfirmation error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onMessageConfirmation(data => {
			ack(data);
		});
	}

	/**
	 * 收到AI识别结果,如是否活体检测通过
	 * @param {function} ack 返回信息确认的具体数据
	 * @example
	 * ivcs.client.onAICheckoutMessage(data => {
		});
	 */
	onAICheckoutMessage(ack) {
		this.ccsClient.onAICheckoutMessage(data => {
			ack(data);
		});
	}

	/**
	 * 监听到机器人开始播报一句话术
	 * @param {function} ack {status,resp,msgId}
	 * @example
	 * ivcs.client.onAiStartSpeak((status,resp,msgId) => {
		});
	 */
	onAiStartSpeak(ack) {
		this.ccsClient.onAiStartSpeak(data => {
			ack(data);
		});
	}

	/**
	 * 监听到机器人一句话术播报完成
	 * @param {function} ack {status,resp,msgId}
	 * @example
	 * ivcs.client.onAiStartDone((status,resp,msgId) => {
		});
	 */
	onAiStartDone(ack) {
		this.ccsClient.onAiStartDone(data => {
			ack(data);
		});
	}

	/**
	 * 监听ccs resume-connect 成功
	 * @param {function} ack 返回当前会话状态
	 */
	onCcsResumeSuccess(ack) {
		this.ccsClient.resumeConnectionSuccess(ack);
	}

	/**
	 * 收到转人工的消息
	 * @param {function} ack  返回转人工的原因消息
	 * @example
	 * ivcs.client.onTransferReasonMessage(data => {
		});
	 */
	onTransferReasonMessage(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.warning(`ivcs-js-sdk:onTransferReasonMessage error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onTransferReason(data => {
			ack(data);
		});
	}

	/**
	 * 添加事件监听
	 * @param {string} eventName  事件名，目前系统支持的事件有:onCustomerJoinSession,onCustomerQuitSession,onAgentQuitSession,onAgentJoinSession,systemError,onStreamEnd,onStreamAdd
	 * @param {function} ack 事件返回具体的数据
	 * @returns {function} 返回事件函数
	 * @example
	 *  // 监听通话中 有流加入
	 *  this.streamAddEvent = ivcs.client.addEventListener('onStreamAdd', streamInfo => {
				console.log('onStreamAdd', streamInfo);
				let label = streamInfo.data.streamLabel; // 流标签，可根据label去订阅这个流画面
				let streamId = streamInfo.data.id; // 流原始id
				let userInfo = streamInfo.data.userInfo;// 流发布者的用户信息，{clientId,userName}
				let audioOnly = streamInfo.data.audioOnly;// 是否是纯语音流
				let isAudioMute = streamInfo.data.isAudioMute; // 流是否是静音 <发布者静音则为true>
				let isVideoMute = streamInfo.data.isVideoMute; // 流视频是否是停止的 <发布者暂停推送视频画面则为true>

		});
		// 监听通话中 有流离开
		this.streamEndEvent = ivcs.client.addEventListener('onStreamEnd', streamInfo => {
				console.log('onStreamEnd', streamInfo);
				let label = streamInfo.data.streamLabel; // 流标签，可根据label去隐藏这个流画面
		});
		// 监听流 语音，视频发生变化。
		this.streamUpdateEvent = ivcs.client.addEventListener('onStreamUpdated', streamInfo => {
				console.log('onStreamUpdated', streamInfo);
				let label = streamInfo.data.streamLabel; // 流标签
				let streamId = streamInfo.data.id; // 流原始id
				let userInfo = streamInfo.data.userInfo;// 流发布者的用户信息，{clientId,userName}
				// 流更新信息
				// updateInfo:[{field:'media.video.status',value:true}] 视频恢复正常状态,
				// updateInfo:[{field:'media.video.status',value:false}] 视频关闭
				// updateInfo:[{field:'media.audio.status',value:true}] 声音恢复正常状态,
				// updateInfo:[{field:'media.audio.status',value:false}] 声音关闭
				let updateInfo = streamInfo.data.info; // 流更新信息，数组类型
		});
	 */
	addEventListener(eventName, ack) {
		Logger.info(`ivcs-js-sdk:add event listener with Name,${eventName}`);
		if (eventName === systemErrorName && this.islogs && this.islogs.isOpen) {
			let event_function = addEventListener(eventName, data => {
				errorlogs('system error ', this.islogs, this.sessionId);
				errorlogs(data, this.islogs, this.sessionId);
				ack(data);
			});
			this.eventList.set(eventName, event_function);
			return event_function;
		} else {
			let event_function = addEventListener(eventName, ack);
			this.eventList.set(eventName, event_function);
			return event_function;
		}
	}

	/**
	 * 移除事件监听
	 * @param {string} eventName 事件名
	 * @param {function} eventImplementationFunction  添加事件监听对应的返回的事件函数
	 * @example
	 * ivcs.client.removeEventListener('onStreamEnd', this.streamEndEvent);
	 */
	removeEventListener(eventName, eventImplementationFunction) {
		removeEventListener(eventName, eventImplementationFunction);
	}

	/**
	 * 收到zms channel的message
	 * @deprecated since version 1.0.2
	 * @param {function} ack 收到zms channel 信息数据
	 */
	onZmsChatMessage(ack) {
		this.irtcClient.messageEvent = addEventListener('irtc_chatmessage', data => {
			ack(data);
		});
	}

	/**
	 * 切换摄像头,本api被publication.upgrade替换,已停止使用
	 * @param {object} newMediaStream  新的视频流
	 * @param {function} onSuccess 成功回调
	 * @param {function} onFailure 失败回调
	 * @ignore
	 */
	switchCameraWithStream(newMediaStream, onSuccess, onFailure) {
		replaceMediaStream(
			this.localStream,
			newMediaStream,
			this.publication,
			resp => {
				onSuccess(resp);
				this.localStream = newMediaStream;
			},
			error => {
				onFailure(error);
			}
		);
	}

	/**
	 * 停止本地流画面
	 * @ignore
	 */
	stopLocalStream() {
		stopMediaStream(this.localStream);
	}

	/**
	 * 更新默认的推流参数
	 * @param {object} mediaOptions 访客端音视频画面的参数
	 * @example mediaOptions:
	 * {
			cameraStreamAudioConstraints:{
				deviceId:undefined,
				source:'mic',
			},
			cameraStreamVideoConstraints:{
				deviceId:undefined,
				frameRate:15,
				resolution:{
					width:640,
					height:480
				},
				source:'camera',
			},
			cameraPublishOptions:{
				audio:[{
					codec:{
						name:'opus'
					},
					maxBitrate:64
				}],
				video:[
					{
						codec:{
							name:'vp8'
						},
						maxBitrate:1024
					},
					{
						codec:{
							name:'h264'
						},
						maxBitrate:1024
					}
				]
			},
			screenStreamAudioConstraints:{
				source:'screen-cast',
			},
			screenStreamVideoConstraints:{
				frameRate:15,
				resolution:{
					width:1280,
					height:720
				},
				source:'screen-cast',
			},
			screenPublishOptions:{
				audio:[{
					codec:{
						name:'opus'
					},
					maxBitrate:64
				}],
				video:[
					{
						codec:{
							name:'vp8'
						},
						maxBitrate:1024
					},
					{
						codec:{
							name:'h264'
						},
						maxBitrate:1024
					}
				]
			},
		}
	*/
	updateMediaOptions(mediaOptions, onSuccess, onFailure) {
		if (mediaOptionsIsValid(mediaOptions) != 'ok') {
			onFailure(mediaOptionsIsValid(mediaOptions));
			return;
		}
		this.mediaOptions = deepCopy(mediaOptions);
		onSuccess(this.mediaOptions);
	}

	/**
	 * 收到截图指令
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据 {createdAt:'',data:{askForPermission:false,cmd:'get-screenshot',cmdId:int('cmdId'),createdAt:''},from:string('clientId'),fromUser:{id,avatar,isRobot,name},id:string('msgId'),to:string('clientId'),type:'peer-cmd'}
	 * @example
	 * ivcs.client.onScreenSnapshot(data => {
			this.saveAgentMessageData(data, 'get-screenshot');//开发者的业务处理示例
		});
	 */
	onScreenSnapshot(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenSnapshot error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('get-screenshot', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送截图结果，截取访客视频流画面
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {picName:string(),url:string()} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseScreenShot(this.cmdList['get-screenshot'], status, responseData, this.agent.clientId, success, error);
	 */
	responseScreenShot(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseScreenShot error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('get-screenshot', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseScreenShot error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到截屏指令
	 * @param {function} ack 返回业务指令携带的数据 {createdAt:'',data:{askForPermission:false,cmd:'get-screencapture',cmdId:int('cmdId'),createdAt:''},from:string('clientId'),fromUser:{id,avatar,isRobot,name},id:string('msgId'),to:string('clientId'),type:'peer-cmd'}
	 * @example
	 * ivcs.client.onScreenSnapshot(data => {
			this.saveAgentMessageData(data, 'get-screencapture');//开发者的业务处理示例
		});
	 */
	onScreenCapture(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenCapture error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('get-screencapture', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送截屏结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {picName:string(),url:string()} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseScreenCapture(this.cmdList['get-screencapture'], status, responseData, this.agent.clientId, success, error);
	 */
	responseScreenCapture(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseScreenCapture error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('get-screencapture', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseScreenCapture error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到推送资源（合同/网页/音频/视频/图片/其他）<AI推送查阅等待的条形码>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据 {createdAt:'',data:{askForPermission:false,cmd:'view',cmdId:int('cmdId'),createdAt:'',data:{name,url,type:enum('page'|'video'...)}},from:string('clientId'),fromUser:{id,avatar,isRobot,name},id:string('msgId'),to:string('clientId'),type:'peer-cmd'}
	 * @example
	 * ivcs.client.onSendViewResource(data => {
			this.saveAgentMessageData(data, 'view');//开发者的业务处理示例
		});
	 */
	onSendViewResource(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSendViewResource error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('view', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消访客查看在线资源 （合同/网页/音频/视频/图片/其他）<AI推送查阅等待的条形码>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelViewReource(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelViewReource(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelViewReource error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('view', resp => {
			ack(resp);
		});
	}

	/**
	 * 响应坐席推送的在线资源 （合同/网页/音频/视频/图片/其他）<AI推送查阅等待的条形码>
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData 默认为undefined
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseViewResource(this.cmdList['view'], status, responseData, this.agent.clientId, success, error);
	 */
	responseViewResource(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseViewResource error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('view', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseViewResource error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到推送引导框
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onSendGuideBox(data => {
			this.saveAgentMessageData(data, 'show-guide-box');//开发者的业务处理示例
		});
	 */
	onSendGuideBox(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSendGuideBox error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('show-guide-box', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到推送在线表单
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onSendOnlineForm(data => {
			this.saveAgentMessageData(data, 'fill-form');//开发者的业务处理示例
		});
	 */
	onSendOnlineForm(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSendOnlineForm error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('fill-form', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消访客在线表单填写
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelOnlineForm(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelOnlineForm(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelOnlineForm error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('fill-form', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送响应填写表单的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功:{formId:string(''),sessionId:string('')}  失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseFillForm(this.cmdList['fill-form'], status, responseData, this.agent.clientId, success, error);
	 */
	responseFillForm(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseFillForm error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('fill-form', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseFillForm error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到邀请手写签名
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onInviteHandWriting(data => {
			this.saveAgentMessageData(data, 'get-handwriting');//开发者的业务处理示例
		});
	 */
	onInviteHandWriting(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onInviteHandWriting error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('get-handwriting', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消访客手写签名
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelHandWriting(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelHandWriting(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelHandWriting error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('get-handwriting', resp => {
			ack(resp);
		});
	}

	/**
	 * 监听访客和Ccs会话状态变化
	 * @param {function} ack 返回{enum:['connected','reconnect_failed','reconnecting']}
	 * @example
	 * ivcs.client.onSessionConnectStatus(status => {
			'connected':'链接成功/重连成功',//第一次触发表示和ccs建立链接,后面触发表示重连成功
			'reconnect_failed':'重连失败',//无法发送任何消息如im和响应对方的业务指令
			'reconnecting':'正在重连中...'//正在重连中,
			'offline':'断网',
			'online':'网络恢复链接'
	   });
	 */
	onSessionConnectStatus(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSessionConnectStatus error', isValid);
			return;
		}
		this.ccsClient.onConnectStatus(resp => {
			ack(resp);
		});
		this.startMonitorNetwork(ack);
	}

	/**
	 * 获取会话中的成员列表
	 * @param {string|object} data 扩展字段，先阶段不需要传
	 * @param {function} onSuccess 返回成员列表
	 * @param {function} onFailure 获取失败
	 */
	getSessionMembers(data = null, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:getSessionMembers error', isValid);
			onFailure(isValid);
			return;
		}
		this.ccsClient.getSessionMembers(data, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:getSessionMembers error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听session中其他用户ccs重连成功,用于检测对方网络是否重连成功
	 * @param {function} ack 
		  data:{
		  id: client.id,
		  status: 'resmue-connect',
		  role: client.role,
		  user: client.user,
		  at: moment().toISOString()
		}
	 * @example
		ivcs.client.onOtherResumeConnect(data => {
		});
	 */
	onOtherResumeConnect(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOtherResumeConnect error', isValid);
			return;
		}
		this.ccsClient.onClientResumeConnectStatus(resp => {
			ack(resp);
		});
	}

	/**
	 * 监听session中其他用户ccs重连中,用于检测对方网络是否断开在重连中
	 * @param {function} ack 
	 * 	data:{
		  id: client.id,
		  status: 'reconnecting',
		  role: client.role,
		  user: client.user,
		  at: moment().toISOString()
		}
	 * @example
		ivcs.client.onOtherReconnecting(data => {
		});
	 */
	onOtherReconnecting(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOtherReconnecting error', isValid);
			return;
		}
		this.ccsClient.onClientReconnectingStatus(resp => {
			ack(resp);
		});
	}

	/**
	 * 发送页面的状态,隐藏｜显示
	 * @param {object} data data {status:'hidden'|'visiable'}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	sendPageStatus(data, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:sendPageStatus error', isValid);
			return;
		}
		this.ccsClient.sendPageStatus(data, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:sendPageStatus error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 发送网络状态
	 * @param {object} data data {status:'weak'|'normal'}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	sendNetworkStatus(data, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:sendNetworkStatus error', isValid);
			return;
		}
		this.ccsClient.sendNetworkStatus(data, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:sendNetworkStatus error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 发送响应手写签名的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功:{picName:string(),url:string()}  失败:{reason:string(),code:string()}  轨迹<status='in-progress'>{stroke: string('jsonstringify')}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseHandWriting(this.cmdList['get-handwriting'], status, responseData, this.agent.clientId, success, error);
	 */
	responseHandWriting(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseHandWriting error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('get-handwriting', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseHandWriting error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到请求获取访客地理位置
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onRequestCustomerLocation(data => {
			this.saveAgentMessageData(data, 'get-geo-location');//开发者的业务处理示例
		});
	 */
	onRequestCustomerLocation(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onRequestCustomerLocation error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('get-geo-location', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消获取访客地理位置
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelRequestCustomerLocation(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelRequestCustomerLocation(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelRequestCustomerLocation error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('get-geo-location', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送获取地理位置的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功:{location:string()}  失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseGeoLocation(this.cmdList['get-geo-location'], status, responseData, this.agent.clientId, success, error);
	 */
	responseGeoLocation(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseGeoLocation error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('get-geo-location', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseGeoLocation error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到请求访客扫码
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * 	ivcs.client.onRequestCustomerScanCode(data => {
			this.saveAgentMessageData(data, 'scan-code');//开发者的业务处理示例
		});
	 */
	onRequestCustomerScanCode(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onRequestCustomerScanCode error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('scan-code', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消访客扫码
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelCustomerScanCode(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelCustomerScanCode(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelCustomerScanCode error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('scan-code', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送扫码的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功:{type: enum('bar-code','QR-code'),result: string(CodeResult)}  失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseScanCode(this.cmdList['scan-code'], status, responseData, this.agent.clientId, success, error);
	 */
	responseScanCode(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseScanCode error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('scan-code', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseScanCode error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到开启共享标注<目前只支持h5>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onScreenMark(data => {
			this.saveAgentMessageData(data, 'screen-mark');//开发者的业务处理示例
		});
	 */
	onScreenMark(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenMark error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('screen-mark', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消共享标注
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelScreenMark(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelScreenMark(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelScreenMark error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('screen-mark', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到屏幕共享<目前只支持h5>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onScreenShare(data => {
			this.saveAgentMessageData(data, 'screen-share');//开发者的业务处理示例
		});
	 */
	onScreenShare(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onScreenShare error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('screen-share', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消屏幕共享<目前只支持h5>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelScreenShare(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelScreenShare(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelScreenShare error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('screen-share', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到内容确认消息<目前只适用于AI推送汇总确认>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onConfirm(data => {
			this.saveAgentMessageData(data, 'confirm');//开发者的业务处理示例
		});
	 */
	onConfirm(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onConfirm error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('confirm', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消内容确认消息<目前只适用于AI推送汇总确认>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelConfirm(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelConfirm(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelConfirm error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('confirm', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送内容确认过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseConfirm(this.cmdList['confrim'], status, responseData, this.agent.clientId, success, error);
	 */
	responseConfirm(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseConfirm error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('confirm', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseConfirm error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到选项判断消息<目前只适用于AI推送选项判断>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onMakeChoice(data => {
			this.saveAgentMessageData(data, 'make-choice');//开发者的业务处理示例
		});
	 */
	onMakeChoice(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onMakeChoice error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('make-choice', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消选项判断消息<目前只适用于AI推送选项判断>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelMakeChoice(data => {
			this.saveAgentMessageData(data, 'peer-cancel');//开发者的业务处理示例
		});
	 */
	onCancelMakeChoice(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelMakeChoice error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('make-choice', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送选项判断的选择结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseMakeChoice(this.cmdList['make-choice'], status, responseData, this.agent.clientId, success, error);
	 */
	responseMakeChoice(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseMakeChoice error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('make-choice', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseMakeChoice error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到同屏浏览<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCoBrowsing(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.coBrowsingCommandId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onCoBrowsing(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCoBrowsing error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('co-browsing', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消同屏浏览<目前用于坐席端同屏辅助>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelCoBrowsing(resp => {
		//开发者的业务处理示例
		console.log(resp);
		this.coBrowsingCommandId = 0;
		});
	 */
	onCancelCoBrowsing(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelCoBrowsing error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('co-browsing', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送同屏浏览过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseCoBrowsing(
			this.coBrowsingCommandId,
			data.action === 'endScreenSync' ? 'done' : 'in-progress',
			data,
			this.agentId,
			success => {
				console.log('responseCoBrowsing success :', success);
			},
			error => {
				console.error('responseCoBrowsing error :', error);
			}
		);
	 */
	responseCoBrowsing(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseCoBrowsing error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('co-browsing', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseCoBrowsing error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 收到同屏浏览标注<目前用于坐席端同屏辅助下标注>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onBrowsingMark(resp => {
			//开发者的业务处理示例
			if (resp && resp.data && resp.data.data) {
				this.messageIn = resp.data.data;
			}
		});
	 */
	onBrowsingMark(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onBrowsingMark error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('browsing-mark', resp => {
			ack(resp);
		});
	}

	/**
	 * 收到取消同屏浏览标注<目前用于坐席端同屏辅助下标注>
	 * @since 1.0.5版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelBrowsingMark(resp => {
		//开发者的业务处理示例
		console.log(resp);
		this.messageIn = null;
		});
	 */
	onCancelBrowsingMark(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onCancelBrowsingMark error', isValid);
			return;
		}
		this.ccsClient.onPeerCancel('browsing-mark', resp => {
			ack(resp);
		});
	}

	/**
	 * 发送同屏浏览标注 过程/结果,需结合具体的业务场景使用
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseBrowsingMark(
			this.commandId,
			'in-progress',
			data,
			this.agentId,
			success => {
				console.log('responseBrowsingMark success :', success);
			},
			error => {
				console.error('responseBrowsingMark error :', error);
			}
		);
	 */
	responseBrowsingMark(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:responseBrowsingMark error', isValid);
			return;
		}
		this.ccsClient.sendPeerResponse('browsing-mark', MsgIdOfCmd, status, responseData, toWhom, (status, resp) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				Logger.info(`ivcs-js-sdk:responseBrowsingMark error,${resp},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 根据dom元素截取屏幕快照,并绘制到录像文件中(可选)
	 * @param {object} element dom元素，如document.querySelector("#content"),document.body等
	 * @param {object} options 截屏参数 如:{ x: 370,y: 0,width: 270,height: 480,useCORS:true,},具体参数参考文档https://html2canvas.hertzen.com/configuration
	 * @param {boolean} addToMixStream 是否需要添加到录像文件中，true情况下,如果需要移除则需要调用removeSnapshotInMixStream
	 * @param {object} addPictureParameter 照片添加到录像文件的参数 如:{ x: 370,y: 0,width: 270,height: 480}
	 * @param {function} onSuccess 成功 {imgCanvas:截图的canvas，pictureId:添加到录像文件后的图片id，可用于移除}
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.snapshot(
			document.body,
			{
				width: window.screen.availWidth, 
				height: window.screen.availHeight,
				x: 0,
				y: 0,
			},
			true,
			{},
			(resp) => {
				console.log('snapshot success', resp);
				document.body.appendChild(resp.imgCanvas);
			},
			(error) => {
				console.log('snapshot error', error);
			}
		);
	 */
	async snapshot(element, options, addToMixStream = true, addPictureParameter, onSuccess, onFailure) {
		try {
			let imgCanvas = await this.snapshotModule.getScreenshots(element, options);
			let pictureId = undefined;
			if (addToMixStream) {
				//todo mix img to mixStream
				let JPEG_OUTPUT_QUALITY = 0.85;
				let base64 = imgCanvas.toDataURL('image/jpeg', { quality: JPEG_OUTPUT_QUALITY });
				let parameter = addPictureParameter;
				parameter.base64 = base64;
				pictureId = await addBase64ImgToMixStream(this.backendUrl, this.roomId, parameter);
			}
			onSuccess({
				imgCanvas: imgCanvas,
				pictureId: pictureId,
			});
		} catch (error) {
			Logger.error('ivcs-js-sdk:snapshot error', error);
			onFailure(error);
		}
	}

	/**
	 * 获取本地流单帧图片
	 * @param {function} onSuccess 成功回调，返回 base64 图片
	 * @param {function} onFailure 失败回调
	 * @param {string} customerStreamBox 访客端本地流标签 id
	 * @param {string} suffix 图片格式，png | jpeg
	 * @param {number} quality 图片质量 0.1 - 1
	 * @example
	 *  ivcs.client.snapshotWithStream(
			(resp) => {
				console.log('snapshotWithStream success', resp);
			},
			(error) => {
				console.log('snapshotWithStream error', error);
			},
			'customer',
			'png',
			0.9
		);
	 */
	async snapshotWithStream(onSuccess, onFailure, customerStreamBox = 'customer', suffix = 'png', quality = 0.92) {
		try {
			let base64 = await this.snapshotModule.getBase64ImgWidthStream(this.localStream, customerStreamBox, `image/${suffix}`, quality);
			onSuccess(base64);
		} catch (error) {
			Logger.error('ivcs-js-sdk:snapshot with stream error', error);
			onFailure(error);
		}
	}

	/**
	 * 混流中移除录像文件
	 * @param {string} pictureId 前序截图时添加到录像文件中的图片id
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.removeSnapshotInMixStream(
			'all',
			res => {
				console.log(res, 'removeSnapshotInMixStream Success');
			},
			err => {
				console.log(err, 'removeSnapshotInMixStream Error');
			}
		);
	 */
	async removeSnapshotInMixStream(pictureId = 'all', onSuccess, onFailure) {
		try {
			await removeBase64ImgInMixStream(this.backendUrl, this.roomId, pictureId);
			onSuccess('ok');
		} catch (error) {
			onFailure(error);
		}
	}

	/**
	 * 接收AI机器人走完当前节点后，返回访客处理该节点的过程和结果数据。<本API依赖于机器人服务发送相关节点数据>
	 * @since 1.0.6 新增
	 * @param {function} ack 返回相关的WorkflowProgress数据
	 * @example
	 * ivcs.client.onWorkflowProgress(
			res => {
				console.log(res, 'onWorkflowProgress Success');
			},
			err => {
				console.log(err, 'onWorkflowProgress Error');
			}
		);
	 */
	onWorkflowProgress(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error(`ivcs-js-sdk:onWorkflowProgress error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onWorkflowProgress(data => {
			ack(data);
		});
	}

	/**
	 * 收到机器人的播收音方式，决定是否需要订阅机器人
	 * @param {function} ack
	 */
	onCallDirection(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error(`ivcs-js-sdk:onCallDirection error,${JSON.stringify(isValid)}`);
			return;
		}
		this.ccsClient.onCallDirection(data => {
			ack(data);
		});
	}

	/**
	 * 邀请其他坐席加入当前会话
	 * @param {object} inviteData 相关参数，指定业务场景/坐席id {service:'123',agentId:'123456'}
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 {'401:'转接超时',402':'其他坐席状态无法接听','404':'未找到坐席','406':'转接的业务场景非工作日','407':'转接的业务场景非工作时间','408':'转接的业务场景不可用'}
	 * @example
	 * let option = {
			agentId: ,//指定的坐席id
			service: ,//业务场景id
			bizSceneName: '',//业务场景名称
			userData: {},
			filter: [
				{
					attribute: '是否机器人',
					rule: 'ne', //不等于
					value: 'true',
				},
			],
		};
		console.log('invite-agent parameter', option);
		ivcs.client.inviteOther(
			option,
			(s) => {
				console.log('inviteOther success', s);
			},
			(e) => {
				console.log('inviteOther error', e);
			}
		);
	 */
	async inviteOther(inviteData, onSuccess, onFailure) {
		if (!inviteData.service) {
			inviteData.service = '';
		}
		if (!inviteData.agentId) {
			inviteData.agentId = '*';
		}
		await inviteOtherAgent(this.ccsClient, inviteData).catch(error => {
			onFailure(error);
		});
		let listener = onInvitationProgress(this.ccsClient, resp => {
			Logger.info('ivcs-js-sdk:invite other agent progress', resp, new Date().toISOString());
			let code = resp.code;
			if (code === '403' || code === '405') {
				this.ccsClient.offInvitationProgress(listener);
				onSuccess(resp);
			} else if (code === '401' || code === '404' || code === '406' || code === '407' || code === '408') {
				this.ccsClient.offInvitationProgress(listener);
				onFailure(resp);
			}
		});
	}

	/**
	 * 取消所有的invite（取消转接和三方）
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 * @example
	 * ivcs.client.cancelInvitations((s)=>{
			console.log('cancelInvitations success',s);
		},(e)=>{
			console.log('cancelInvitations error',e)
		})
	 */
	cancelInvitations(onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
		}
		this.ccsClient.cancelInvitations(undefined, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess();
			} else {
				Logger.error(`ivcs-js-sdk:cancel invitations failed,${resp},msgId ${msgId},${new Date().toISOString()}`);
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听AI流程经过nlp节点
	 * @since 1.0.8版本新增API
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onNlpNode(resp => {
			//开发者的业务处理示例
			if (resp && resp.data && resp.data.data) {
				this.messageIn = resp.data.data;
			}
		});
	 */
	onNlpNode(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onNlpNode error', isValid);
			return;
		}
		this.ccsClient.onPeerCmd('third-nlp', resp => {
			ack(resp);
		});
	}

	/**
	 * 开启本地录制
	 * @param {string} streamLabel 流标签，可根据流标签，选择本地录制的画面。
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 {400:未开启音视频会话,401:开启失败}
	 * @example
	 * ivcs.client.startLocalRecordWithStreamLabel(
				'customer',
				s => {
					console.log('startLocalRecordWithStreamLabel success', s);
				},
				e => {
					console.log('startLocalRecordWithStreamLabel error', e);
				}
			);
	 */
	startLocalRecordWithStreamLabel(streamLabel, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		startLocalRecordWithStreamLabel(this.irtcClient, streamLabel, onSuccess, e => {
			onFailure(new IvcsError('401', e));
		});
	}
	/**
	 * 停止本地录制
	 * @param {string} streamLabel 流标签，可根据流标签，选择停止本地录制的画面。
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 {400:未开启音视频会话,401:停止失败}
	 * @example
	 * ivcs.client.stopLcoalRecordWithStreamLabel(
				'customer',
				s => {
					console.log('stopLcoalRecordWithStreamLabel success', s);
				},
				e => {
					console.log('stopLcoalRecordWithStreamLabel error', e);
				}
			);
	 */
	stopLcoalRecordWithStreamLabel(streamLabel, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		stopLcoalRecordWithStreamLabel(this.irtcClient, streamLabel, onSuccess, e => {
			onFailure(new IvcsError('401', e));
		});
	}

	/**
	 * 下载本地录制视频到本地
	 * @param {string} streamLabel 流标签，可根据流标签，选择停止本地录制的画面。
	 * @param {string} fileName 下载到本地文件名
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败 {400:未开启音视频会话,401:下载失败}
	 * @example
	 * ivcs.client.downloadLcoalRecordWithStreamLabel(
				'customer',
				'customer',
				s => {
					console.log('downloadLcoalRecordWithStreamLabel success', s);
				},
				e => {
					console.log('downloadLcoalRecordWithStreamLabel error', e);
				}
			);
	 */
	downloadLcoalRecordWithStreamLabel(streamLabel, fileName, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		downloadLcoalRecordWithStreamLabel(this.irtcClient, streamLabel, fileName, onSuccess, e => {
			onFailure(new IvcsError('401', e));
		});
	}

	/** 
	 * 返回推流的状态数据，可根据2次间隔之间的指标参数变化判断网络状态
	 * @param {number} milliseconds 返回的时间间隔，单位(毫秒)，默认3000毫秒。即每隔3秒返回实时流的相关状态数据
	 * @param {function} onSuccess {actualBitrate:实际码率(单位kbps),actualHeight:发送的视频分辨率的高,actualWidth:发送的视频分辨率的宽,bytesSent:一共发送的的字节数,packetsSent:一共发送的数据包数,packetsLost:一共丢的数据包数,rate:实时的视频帧率,packetsLostRate:丢包率}
	 * @param {function} onFailure code 错误说明{401:当前流未推送,402:已经正在获取推流数据中,无须再次调用,403:publication.getStats失败,400:暂无音视频会话/音视频会话已结束}
	 * @returns {number} intervalId 返回定时任务id,作为移除该任务的参数。
	 * @example
	 * let milliseconds = 3000;
	   ivcs.client.getLocalStreamStats(
			milliseconds,
			(resp) => {
				console.log('getLocalStreamStats,success',resp);
			},
			(error) => {
				console.log('getLocalStreamStats,failed', error);
			}
		);
	 */
	getLocalStreamStats(milliseconds = 3000, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		return this.irtcClient.getPublicationStats(this.publication, milliseconds, onSuccess, onFailure);
	}

	/**
	 * 停止获取推流的状态数据
	 * @param {number} intervalId
	 * @param {function} onSuccess 'ok'
	 * @param {function} onFailure code 错误说明{401:当前流未推送,402:intervalId无效,400:暂无音视频会话/音视频会话已结束}
	 * @example
	 * ivcs.client.stopLocalStreamStats(
			getStatsIntervalId,
			(resp) => {
				console.log('stopLocalStreamStats,success', resp, new Date());
			},
			(error) => {
				console.log('stopLocalStreamStats,failed', error);
			}
		);
	 */
	stopLocalStreamStats(intervalId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		this.irtcClient.stopPublicationStats(this.publication, intervalId, onSuccess, onFailure);
	}

	/** 
	 * 返回订阅流的状态数据，可根据2次间隔之间的指标参数变化判断网络状态
	 * @param {string} streamLabel 已经渲染的视频标签
	 * @param {number} milliseconds 返回的时间间隔，单位(毫秒)，默认3000毫秒。即每隔3秒返回实时流的相关状态数据
	 * @param {function} onSuccess {actualHeight:发送的视频分辨率的高,actualWidth:发送的视频分辨率的宽,packetsLost:一共丢的数据包数,rate:实时的视频帧率,delayMs:时延}
	 * @param {function} onFailure code 错误说明{401:已经渲染的视频流不存在或者异常,402:已经正在获取数据中,无须再次调用,403:subscription.getStats失败,400:暂无音视频会话/音视频会话已结束}
	 * @returns {number} intervalId 返回定时任务id,作为移除该任务的参数。
	 * @example
	 * let milliseconds = 3000;
	   ivcs.client.getRemoteStreamStats(
		   'agent001',
			milliseconds,
			(resp) => {
				console.log('getRemoteStreamStats,success',resp);
			},
			(error) => {
				console.log('getRemoteStreamStats,failed', error);
			}
		);
	 */
	getRemoteStreamStats(streamLabel, milliseconds = 3000, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		let subscription = this.irtcClient.subscriptionList.get(streamLabel);
		return this.irtcClient.getSubscriptionStats(subscription, milliseconds, onSuccess, onFailure);
	}

	/**
	 * 停止获取推流的状态数据
	 * @param {string} streamLabel 已经渲染的视频标签
	 * @param {number} intervalId
	 * @param {function} onSuccess 'ok'
	 * @param {function} onFailure code 错误说明{401:已经渲染的视频流不存在或者异常,402:intervalId无效,400:暂无音视频会话/音视频会话已结束}
	 * @example
	 * ivcs.client.stopRemoteStreamStats(
			getStatsIntervalId,
			(resp) => {
				console.log('stopRemoteStreamStats,success', resp, new Date());
			},
			(error) => {
				console.log('stopRemoteStreamStats,failed', error);
			}
		);
	*/
	stopRemoteStreamStats(streamLabel, intervalId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		let subscription = this.irtcClient.subscriptionList.get(streamLabel);
		this.irtcClient.stopSubscriptionStats(subscription, intervalId, onSuccess, onFailure);
		this.irtcClient.subscriptionList.delete(streamLabel);
	}

	/**
	 * 修改订阅参数
	 * @param {string} streamLabel
	 * @param {object} options
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	applyOptions(streamLabel, options, onSuccess, onFailure) {
		if (!this.irtcClient) {
			onFailure(new IvcsError('400', 'irtcClient is invalid'));
			return;
		}
		let subscription = this.irtcClient.subscriptionList.get(streamLabel);
		if (!subscription) {
			onFailure(new IvcsError('400', 'streamlabel is invalid'));
			return;
		}
		subscription
			.applyOptions(options)
			.then(resp => {
				onSuccess(resp);
			})
			.catch(error => {
				onFailure(new IvcsError('400', error));
			});
	}

	/**
	 * 以特定from_clientId 发送消息
	 * @ignore
	 * @param {string} message 消息内容
	 * @param {string} from  为空，则默认是当前用户发送的消息
	 * @param {string} toRole 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 成功
	 * @param {function} onFailure 失败
	 */
	sendTextMessageWithFromClientId(message, from, toRole = 'customers', onSuccess, onFailure) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			onFailure(isValid);
			return;
		}
		// eslint-disable-next-line no-unused-vars
		this.ccsClient.sendMessageFromTo('text', message, from, toRole, (status, resp, msgId) => {
			if (status === 'ok') {
				onSuccess(resp);
			} else {
				dealWithCcsSendMessageResponse(status, resp, onFailure);
			}
		});
	}

	/**
	 * 监听到ocr识别的结果
	 * @param {function} ack {object}
	 */
	onOcrRecord(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onOcrRecord error', isValid);
			return;
		}
		this.ccsClient.onOcrRecord(resp => {
			ack(resp);
		});
	}

	/**
	 * 收到采集身份证
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCollectIdCard(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onCollectIdCard(ack) {
		onPeerCmd(this.ccsClient, 'idcard-collection', ack);
	}

	/**
	 * 收到取消采集身份证
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCollectIdCard(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
		*/
	onCancelCollectIdCard(ack) {
		onCancelPeerCmd(this.ccsClient, 'idcard-collection', ack);
	}

	/**
	 * 响应身份证采集的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg',position:('front','back'))]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseCollectIdCard(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responseCollectIdCard success :', success);
			},
			error => {
				console.error('responseCollectIdCard error :', error);
			}
		);
		*/
	responseCollectIdCard(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'idcard-collection', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 收到采集银行卡
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCollectBankCard(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onCollectBankCard(ack) {
		onPeerCmd(this.ccsClient, 'bankcard-collection', ack);
	}

	/**
	 * 收到取消采集银行卡
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelCollectBankCard(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
		*/
	onCancelCollectBankCard(ack) {
		onCancelPeerCmd(this.ccsClient, 'bankcard-collection', ack);
	}

	/**
	 * 响应银行卡采集的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg')]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseCollectBankCard(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responseCollectBankCard success :', success);
			},
			error => {
				console.error('responseCollectBankCard error :', error);
			}
		);
	*/
	responseCollectBankCard(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'bankcard-collection', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 获取后置摄像头视频流，用于语音下，拿后置摄像头流去做业务操作
	 * @param {function} onSuccess mediaStream
	 * @param {function} onFailure {70000:获取后置摄像头流失败}
	 * @example
	 * ivcs.client.getRearCameraVideoStream(
			success => {
				console.log('getRearCameraVideoStream success :', success);
			},
			error => {
				console.error('getRearCameraVideoStream error :', error);
			}
		);
	 */
	getRearCameraVideoStream(onSuccess, onFailure) {
		this.cameraNavigator.getRearCameraVideoStream(onSuccess, onFailure);
	}

	/**
	 * 释放流的track，即释放摄像头的占用。
	 * @param {MediaStream} stream
	 * @example
	 * ivcs.client.releaseStreamTrack(this.stream);
	 */
	releaseStreamTrack(stream) {
		stopStream(stream);
	}

	/**
	 * 监听智能识别的文本，如asr，tts，预录制视频的文本
	 * @param {function} ack 返回内容 {createdAt:string(),data:string(),from:string(clientId),fromUser:Object(),to:string(),type:string('text-asr','text-tts','text-pre-recorded')}
	 * @example
	 * ivcs.client.onAiRecognitionMessage(resp => {
			if (resp && resp.data) {
				this.textMessage = resp.data;
			}
			this.textType = resp.type; // 'text-asr','text-tts','text-pre-recorded'	
		});
	 */
	onAiRecognitionMessage(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onAiRecognitionMessage error', isValid);
			return;
		}
		this.ccsClient.onAsrTextMessage(ack);
		// 后端text-ttx数据结构变更
		this.ccsClient.onTtsTextMessage(data => {
			let resultData = deepCopy(data);
			resultData.data = data?.data?.content ? data?.data?.content : data?.data;
			ack(resultData);
		});
		this.ccsClient.onPreRecordVideoTextMessage(ack);
	}

	/**
	 * 打开白板
	 * @param {string} whiteboardId 白板id
	 * @param {string} hostname 白板的前端部署主机地址 如：'https://www.abc.com'。
	 * @param {function} onSuccess  成功iframe
	 * @param {function} onFailure  失败{code,message}
	 */
	openWhiteboard(whiteboardId, hostname, onSuccess, onFailure) {
		if (!this.irtcClient) {
			Logger.error('ivcs-js-sdk:not join room');
			return;
		}
		if (!this.irtcWhiteBoard) {
			Logger.error('ivcs-js-sdk:not startWhiteboard');
			return;
		}
		this.irtcWhiteBoard.startWithId(whiteboardId, hostname, onSuccess, onFailure);
	}

	/**
	 * 打开pdf白板
	 * @param {string} whiteboardId 白板id
	 * @param {string} hostname 白板的前端部署主机地址 如：'https://www.abc.com'。
	 * @param {function} onSuccess  成功iframe
	 * @param {function} onFailure  失败{code,message}
	 */
	openPdfWhiteboard(whiteboardId, hostname, onSuccess, onFailure) {
		this.openWhiteboard(whiteboardId, hostname, onSuccess, onFailure);
	}

	/**
	 * 打开证言抄录白板
	 * @param {string} whiteboardId 白板id
	 * @param {string} hostname 白板的前端部署主机地址 如：'https://www.abc.com'。
	 * @param {function} onSuccess  成功iframe
	 * @param {function} onFailure  失败{code,message}
	 */
	openTestimonyWrite(whiteboardId, hostname, onSuccess, onFailure) {
		this.openWhiteboard(whiteboardId, hostname, onSuccess, onFailure);
	}

	/**
	 * 创建证言白板
	 * @param {string} name 证言白板名称，可任意填写。
	 * @param {string} path 白板的前端页面部署的path, 如：'/customer/static/whiteboard'。
	 * @param {string} hostname 白板的前端部署主机地址, 如：'https://www.abc.com'。
	 * @param {function} onSuccess {whiteboardId,iframe}
	 * @param {function} onFailure {code,message}
	 */
	async createTestimonyWriteboard(name, path, hostname, onSuccess, onFailure) {
		if (!this.irtcClient) {
			Logger.error('ivcs-js-sdk:not join room');
			return;
		}
		try {
			!this.irtcWhiteBoard ? (this.irtcWhiteBoard = new IrtcWhiteBoard(this.irtcClient)) : '';
			let whiteboard = await this.irtcWhiteBoard.create(name, path);
			let iframe = await this.irtcWhiteBoard.start(whiteboard, hostname);
			onSuccess({
				whiteboardId: whiteboard.id,
				iframe: iframe,
			});
		} catch (error) {
			Logger.error(`ivcs-js-sdk:startWhiteboard error,at:${new Date().toISOString()}`);
			onFailure(error);
		}
	}

	/**
	 * 截图证言抄录白板
	 * @param {string} whiteboardId 白板id
	 * @param {function} onSuccess  成功 {front:string(base64),background,metaData}
	 * @param {function} onFailure  失败{code,message}
	 */
	snapshotWhiteboard(whiteboardId, onSuccess, onFailure) {
		if (!this.irtcClient) {
			Logger.error('ivcs-js-sdk:not join room');
			return;
		}
		if (!this.irtcWhiteBoard) {
			Logger.error('ivcs-js-sdk:not startWhiteboard');
			return;
		}
		this.irtcWhiteBoard.snapshotWithId(whiteboardId, onSuccess, onFailure);
	}
	/** 
	 * 收到采集变量
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCollectVariable(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onCollectVariable(ack) {
		onPeerCmd(this.ccsClient, 'collect-variable', ack);
	}

	/**
	 * 收到取消变量采集
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelCollectVariable(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
		*/
	onCancelCollectVariable(ack) {
		onCancelPeerCmd(this.ccsClient, 'collect-variable', ack);
	}

	/**
	 * 响应变量采集的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg')]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseCollectVariable(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responseCollectVariable success :', success);
			},
			error => {
				console.error('responseCollectVariable error :', error);
			}
		);
	*/
	responseCollectVariable(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'collect-variable', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/** 
	 * 收到短视频采集
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCollectShortVideo(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onCollectShortVideo(ack) {
		onPeerCmd(this.ccsClient, 'collect-short-video', ack);
	}

	/**
	 * 收到取消短视频采集
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelCollectShortVideo(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
		*/
	onCancelCollectShortVideo(ack) {
		onCancelPeerCmd(this.ccsClient, 'collect-short-video', ack);
	}

	/**
	 * 响应短视频采集的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg')]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseCollectShortVideo(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responseCollectShortVideo success :', success);
			},
			error => {
				console.error('responseCollectShortVideo error :', error);
			}
		);
	*/
	responseCollectShortVideo(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'collect-short-video', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 收到人工坐席共享白板
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onWhiteboard(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onWhiteboard(ack) {
		onPeerCmd(this.ccsClient, 'whiteboard', ack);
	}

	/**
	 * 收到取消共享白板
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelWhiteboard(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
		*/
	onCancelWhiteboard(ack) {
		onCancelPeerCmd(this.ccsClient, 'whiteboard', ack);
	}

	/**
	 * 响应白板共享的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg')]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseWhiteboard(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responseWhiteboard success :', success);
			},
			error => {
				console.error('responseWhiteboard error :', error);
			}
		);
	*/
	responseWhiteboard(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'whiteboard', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 收到人工坐席pdf标注白板
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onPdfWhiteboard(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onPdfWhiteboard(ack) {
		onPeerCmd(this.ccsClient, 'whiteboard-pdf', ack);
	}

	/**
	 * 收到取消共享pdf白板
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelPdfWhiteboard(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
		*/
	onCancelPdfWhiteboard(ack) {
		onCancelPeerCmd(this.ccsClient, 'whiteboard-pdf', ack);
	}

	/**
	 * 响应pdf白板共享的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg')]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responsePdfWhiteboard(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responsePdfWhiteboard success :', success);
			},
			error => {
				console.error('responsePdfWhiteboard error :', error);
			}
		);
	*/
	responsePdfWhiteboard(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'whiteboard-pdf', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 收到人工坐席pdf标注白板
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onTestimonyWrite(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onTestimonyWrite(ack) {
		onPeerCmd(this.ccsClient, 'testimony-write', ack);
	}

	/**
	 * 收到取消共享pdf白板
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelTestimonyWrite(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
	*/
	onCancelTestimonyWrite(ack) {
		onCancelPeerCmd(this.ccsClient, 'testimony-write', ack);
	}

	/**
	 * 响应证言抄录的过程/结果
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData  成功 {urls:[string('idcardImg')]} 失败:{reason:string(),code:string()}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseTestimonyWrite(
			this.cmdId,
			'done',
			data,
			this.agentId,
			success => {
				console.log('responseTestimonyWrite success :', success);
			},
			error => {
				console.error('responseTestimonyWrite error :', error);
			}
		);
	*/
	responseTestimonyWrite(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'testimony-write', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}

	/**
	 * 设置订阅的是否 停止订阅or恢复订阅，一般用于访客端退出到后台则停止订阅，回到前台则恢复订阅
	 * @param {boolean} isMute 停止or恢复
	 * @param {string} trackKind  video/audio
	 */
	setSubscriptionMute(isMute = true, trackKind = 'video') {
		if (!this.irtcClient) {
			Logger.error('ivcs-js-sdk:not join room');
			return;
		}
		this.irtcClient.setSubscriptionMute(isMute, trackKind);
	}

	/**
	 * 设置资源文件的内外网替换地址
	 * @param {string} resourceOriginUrlDomain 'ivcs-nginx'
	 * @param {string} resourceReplaceUrlDomain '公网-domain'，	'内网-domain'
	 */
	setReplaceOrigin(resourceOriginUrlDomain, resourceReplaceUrlDomain) {
		if (!this.ccsClient) {
			console.error('setReplaceOrigin error,no ccs client');
			return;
		}
		this.ccsClient.setReplaceOrigin(resourceOriginUrlDomain, resourceReplaceUrlDomain);
	}

	/**
	 * 请求升级
	 * @param {string} media "media": "video"/ "audio",
	 * @param {string} toWhom 发送给谁 clientId具体的用户 'customers'= 访客;'agents'= 坐席;'others'= 除了自己的所有人; 'all' = 所有人;
	 * @param {function} onSuccess
	 * @param {function} onFailure {code:60000<音视频创建出错>}
	 */
	requestUpgrade(media, toWhom, onSuccess, onFailure) {
		this.imUpgradeMedia.requestUpgrade(media, toWhom, onSuccess, onFailure, this.ccsClient);
	}

	/**
	 * 接受对方的升级请求
	 * @param {string} media "media": "video"/ "audio",
	 * @param {function} onSuccess
	 * @param {function} onFailure {code:'60000',message:AbortError［中止错误］;NotAllowedError［拒绝错误］;NotFoundError［找不到错误］;NotReadableError［无法读取错误］;OverconstrainedError［无法满足要求错误］;SecurityError［安全错误］}
	 */
	acceptUpgrade(media, onSuccess, onFailure) {
		this.imUpgradeMedia.accept(
			media,
			data => {
				this.roomId = data?.roomId;
				this.roomToken = data?.roomToken;
				this.systemRecordId = data?.recordingId;
				onSuccess(data);
			},
			onFailure,
			this.ccsClient
		);
	}

	/**
	 * 拒绝接听
	 * @param {string} media "media": "video"/ "audio",
	 * @param {object} reason {code,message}
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	rejectUpgrade(media, reason, onSuccess, onFailure) {
		this.imUpgradeMedia.reject(media, reason, onSuccess, onFailure, this.ccsClient);
	}

	/**
	 * 取消请求升级
	 * @param {string} reason 默认‘normal’
	 * @param {function} onSuccess
	 * @param {function} onFailure
	 */
	cancelUpgrade(reason = 'normal', onSuccess, onFailure) {
		this.imUpgradeMedia.cancel(reason, onSuccess, onFailure, this.ccsClient);
	}

	/**
	 * 监听升降级的事件
	 * @param {function} ack {type:'request-upgrade'|'accept-upgrade'|'reject-upgrade'|'cancel-upgrade',from:{},to:{},data:{media,roomId,roomToken,reason}}
	 */
	onUpgrade(ack) {
		this.imUpgradeMedia.onUpgrade(data => {
			Logger.info('ivcs-js-sdk onUpgrade data', data, new Date().toISOString());
			if (data?.data?.type === 'accept-upgrade') {
				this.roomId = data?.data?.roomId;
				this.roomToken = data?.data?.roomToken;
			}
			ack(data);
		}, this.ccsClient);
	}

	/**
	 * 收到当前会话发生业务变化，如坐席推送了扫码，访客拒绝扫码等指令或者坐席操作等消息
	 * @param {function} ack  返回具体数据  ack.data:{
				createdAt:'',
				data:{createdAt:'',data:{},fromClientId:'',fromUserId:'',fromUserInfo:{},fromUserName:'',fromUserRole:'',msgType:'',toUserInfo:{}},
				from:'system',
				fromUser:'',
				id: "830969657034878800"
				to: "all"
				type: "sync-message"
			} 
	 * @example
	 * ivcs.client.onSyncMessage(data => {
			console.log('onSyncMessage data', data);
		});
	 */
	onSyncMessage(ack) {
		let isValid = validateCcsClient(this.ccsClient);
		if (isValid != 'ok') {
			Logger.error('ivcs-js-sdk:onSyncMessage error', isValid);
			return;
		}
		this.ccsClient.onSyncMessage(data => {
			ack(data);
		});
	}

	/**
	 * 收到坐席对当前访客画面实时标注。
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCustomerViewMark(resp => {
		//开发者的业务处理示例
		if (resp && resp.data) {
			this.cmdId = resp.data.cmdId;
			this.agentId = resp.from;
		}
		});
	 */
	onCustomerViewMark(ack) {
		onPeerCmd(this.ccsClient, 'customer-view-mark', ack);
	}

	/**
	 * 收到坐席结束当前访客画面实时标注。
	 * @param {function} ack 返回业务指令携带的数据
	 * @example
	 * ivcs.client.onCancelCustomerViewMark(resp => {
			//开发者的业务处理示例
			console.log(resp);
		});
	*/
	onCancelCustomerViewMark(ack) {
		onCancelPeerCmd(this.ccsClient, 'customer-view-mark', ack);
	}

	/**
	 * 响应坐席请求对访客画面实时标注
	 * @param {string} MsgIdOfCmd 对方发送的业务指令id
	 * @param {string} status enum(['accepted','rejected','in-progress','failed','done'])
	 * @param {object} responseData {}
	 * @param {string} toWhom 发送给谁 string(clientId) = 会话中指定的用户 'customers' = 访客;'agents' = 坐席;'others' = 除了自己的所有人; 'all' = 所有人
	 * @param {function} onSuccess 发送成功
	 * @param {function} onFailure 发送失败
	 * @example
	 * ivcs.client.responseCustomerViewMark(
			cmdId,
			'accepted',
			{},
			agentId,
			success => {
				console.log('responseCustomerViewMark success :', success);
			},
			error => {
				console.error('responseCustomerViewMark error :', error);
			}
		);
	*/
	responseCustomerViewMark(MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure) {
		responsePeerCmd(this.ccsClient, 'customer-view-mark', MsgIdOfCmd, status, responseData, toWhom, onSuccess, onFailure);
	}
}

export { Client };
